You wrote:
To make the generic repository DbRepo(T, U) (where T is the entity and
U is the type of the primary key) work with these changes...
I'm not sure whether you meant a generic class DbRepo<T, U>
, or whether you want a DbRepo
class where you give the entity type and id type in the constructor like this:
class DbRepo
{
public DbRepo(Type entityType, Type idType) {...}
...
}
This method is a bit strange. Once you've decided about the entity type, you have already decided about the type of the id. If in your software version Customers have a Guid Id, then selecting that you'd like a DbRepo for a Customer, then this would automatically mean that you want a GUID type of Id, so why mention it specifically?
I'll leave this problem for later.
Full Generic DbRepo class
A real generic class with generic parameters for the entity type and for the type of the id would be easy and completely type safe. However, you'll have to inform the class which property contains the Id.
class DbRepo<Tentity, Tid> where Tentity : class
{
...
}
Your Single function uses a GetAll() without parameters. I'm not sure whether all your functions in one DbRepo object would use the same GetAll without parameters. You didn't even specify whether all functions in your DbRepo would use the same GetAll(). Therefore, I'll have two functions. One with a GetAll that you provide, and one without a GetAll. This one uses a default GetAll that is set using a property.
A DbRepo with a Single and a Where as extra example, to show full type safeness
class DbRepo<Tentity, Tid> where Tentity : class
{
public Func<IQueryable<Tentity>> DefaultGetAll {get; set;}
public TEntity Single(Tid id)
{
return this.Single(id, this.DefaultGetAll);
}
public Tentity Single(Tid id, Func<IQueryable<Tentity>> getAll)
{
return getAll()
.Where(item => item.Id == id)
.Single();
}
public IReadonlyList<Tentity> Where(Expression<Func<TEntity, bool>> predicate)
{
return this.Where(predicate, this.DefaultGetall);
}
public IReadonlyList<Tentity> Where(Expression<Func<TEntity, bool>> predicate,
Func<IQueryable<Tentity>> getAll)
{
return getAll().Where(predicate).ToList();
}
}
TODO: write what to do if DefaultGetAll is not initialized
As written: this class is fully type safe. If you try to combine Tentity with an incorrect Tid, your compiler will complain in the Where part of the Single function.
If for instance you are using a Customer with a Guid Id, and you try to Single, you can't type an int as Id. Besides the return value is guaranteed a Customer with a Guid Id
Because I think that while typing your code, you are fully aware about the type of the entity and the type of the Id of this entity. Hence this is the safest method to use: if your compiler accepts it you are certain that you didn't make any errors regarding the types.
DbRepo with types in the constructor
You'll see me mentioning a lot of problems you'll meet if you'll use this method. I strongly recommend not using it.
So apparently, at compile time you have several types of Customers, where at least their Id type is different. Somehow you don't know the type of the Customer as a Generic type (otherwise you would use the solution above), but luckily (well...) you do know the type of the customer as a variable.
You want your Single function to return a Customer. If your variable that contained the type said that you wanted a DbRepo for Guid-Customer, then the Single function should return a different type than if your variable has said you'd wanted a DbRepo for an int-Customer.
Of course one Single function can't return two different types, unless you'd use generic type (back to solution mentioned above), or you'd return something that all Customers have in common: a BaseCustomer class, or an ICustomer interface. The Id is not part of the interface, nor the base class.
I think it is easiest if you let all your Customers implement the ICustomer.
interface ICustomer
{
string Name {get; set;}
...
}
class GuidCustomer : ICustomer
{
public Guid Id {get; set;}
// Todo: implement ICustomer
}
class IntCustomer : ICustomer
{
public int Id {get; set;}
// Todo: implement ICustomer
}
Once again: a layout like this scream for a generic Customer type where the type of the Id is the generic parameter. This would bring us back to the first solution.
Problems: Apart from that you'll have to edit all your Customers classes, the users of your Single functions also need to be edited: they should expect an ICustomer instead of a Customer. The users can't access the Id of the customer anymore, after all: it is not in the interface. But since they didn't know the type of the returned Customer, they couldn't access the Id anyway (unless you'd use generics, back to solution 1).
But let's be stubborn and create a function that will return the correct ICustomer
I'll do this using a factory-like pattern. Depending on the parameters I return the correct DbRepo:
class DbRepoFactory
{
public static DbRepo<ICustomer DbRepo(Type entityType)
where entityType : ICustomer
{
PropertyInfo idProperty = entityType.GetProperty("Id");
// unsafe, compiler can't check that your type has a property Id
Type idType = idProperty.PropertyType;
if (idType == typeof(Guid)
return new DbRepo<GuidCustomer, Guid>();
// uses the first solution again
else if (idType == typeof(int)
...
}
}
Although this method would work, it seems to me a lot more work than the generic method I wrote above and it is type unsafe, you will only detect errors at runtime.
I can hardly believe that while instantiating the DbRepo object you don't know whether you'd want a GuidCustomer or an IntCustomer. Therefore I'd recommend you to reconsider your design and go for the generic type solution.
T
is already of the correct type. You could add a generic type argument toGetAll
so it returns aIQueryable<TActual>
instead. – poke