0
votes

In an application that uses Entity Framework 6, I have recently decided to change the primary key of some entities from Guid to int. To avoid creating a second EntityBase class for that and duplicate a lot of code, I modified the already existing EntityBase to EntityBase(T) (where T determines the type of the ID).

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 modified several methods, including the Single method which returns a single entity by its Id (the primary key):

public T Single(U id)
    {
        if (typeof(U) == typeof(Guid))
        {
            return GetAll()
                .Single(e => (e as EntityBase<Guid>).Id == new 
                                       Guid(id.ToString()));
        }
        else
        {
            return GetAll()
                .Single(e => (e as EntityBase<int>).Id == 
                                       int.Parse(id.ToString()));
        }
    }

This is the GetAll() method:

public IQueryable<T> GetAll(Expression<Func<T, bool>> predicate = null, bool skipAccessCheck = false, bool includeDeleted = false)
    {
        IQueryable<T> query = dbSet;


        if (predicate != null)
        {
            query = query.Where(predicate);
        }

        if (!includeDeleted)
        {
            query = query.Where(x => !x.Deleted);
        }

        if (!skipAccessCheck)
        {
            query = Filter(query);
        }

        return query;
    }

However, when I try to call Single(id) to get an entity, the following exception is thrown:

The 'TypeAs' expression with an input of type 'SomeEntity' and a check of type 'EntityBase`1[[System.Guid, >mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]' >is not supported. Only entity types and complex types are supported in LINQ to Entities queries.

Is there a way that I can possibly cast my entity to its EntityBase and run the LINQ query successfully?

1
May I ask what is the reason to switch from Guid to ID and accept the inconsistency of primary key types in your DB?Martin Braun
Basically to save disk space. Guid Ids would be used where the entity has an edit page, so that users could not simply change the url and navigate to next entity. Int Ids would be used for the rest of the tables, which usually have lots of records.M Bakardzhiev
You cannot do the cast within the query; you will have to do that outside. So you have to make sure that the T is already of the correct type. You could add a generic type argument to GetAll so it returns a IQueryable<TActual> instead.poke
@poke Yes, but this would require a lot of changes in a lot of other methods that expect a more generic type of IQueryable and I don't want to cast all the time. I will reconsider my architecture of entity hierarchy. I believe the actual reason for exception is that when I do the cast, Entity Framework is preparing a query for EntityBase<Guid> while such table does not exist in the database.M Bakardzhiev
No… the reason is that you are doing a type cast within the expression and since EntityFramework is parsing the expression tree to build an actual database query, this will not work.poke

1 Answers

1
votes

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.