9

Version Info:

I am using C# 4.5, Entity Framework 6.0, and MEF.

Code and Unit Test

I created a Test Project to explain the problem: https://skydrive.live.com/redir?resid=E3C97EC293A34048!2234

Please Open the UnitTest project and try to run TestIfItWorks() unit test.

Problem

I want to convert a non-generic DbSet to its generic version but I am getting the following exception: InvalidCastException: Cannot create a DbSet<IUser> from a non-generic DbSet for objects of type 'User':

var nonGeneric = context.Set(typeof(User));
var generic = nonGeneric.Cast<IUser>(); //Exception in here

The User class is implementing IUser so you would think the cast shouldn't be a problem unless DbSet code is restricted to concrete classes (I hope not otherwise I need to either create a wrapper around non-generic DbSet to convert it to a generic DbSet or find an alternative to current DbSet implementation).

If you are wondering why I am using interfaces even though they are not currently supported by Microsoft I give you a little explanation (hopefully this would filter out responses that say "Don't Do That" instead of providing a solution) :

I am using MEF and EntityFramework to create a loosely coupled data layer engine through which I can provide Entities (and their corresponding configurations) per project basis. I have been using Interfaces extensively to define the engine. The meta data and concrete implementation of entities in context are discovered in run time using MEF.

Excerpt from code

[TestMethod]
public void TestIfItWorks()
{
    //TODO: Please open the App.Config and change the PluginsPath to match the Plugins folder in your machine.

    using (var dbContext = new MyContext()) //Please ignore this line for now. This was UnitOfWork which I replaced with Context to create a simple unit test
    {
        dbContext.Setup(); //Please ignore this line for now. This was part of UnitOfWork which I moved to here to create a simple unit test

        //The purpose of all these is to be able to read and write user info from/to database while User class is defined in an external assembly
        //but we can import it by MEF using IUser interface.

        //Failed Attempt# 1: Use User class directly! This doesnt work because User is in an external class which we dont have reference to
        //var failedAttempt1 = dbContext.Set<User>(); 

        //Failed Attempt# 2: But the good thing is that we have access to IUser and its exports
        //then lets get a DbSet<IUser> instead
        var failedAttempt2 = dbContext.Set<IUser>();
        try
        {
            var throwsException2 = failedAttempt2.FirstOrDefault();
        }
        catch (InvalidOperationException ex)
        {
            //InvalidOperationException: 
            // The entity type IUser is not part of the model for the current context.
            // It also didnt work when I tried to define a class that inherits from EntityTypeConfiguration<IUser>at TestImplementation
        }




        //Ok then lets do it differently this time. Lets get User type (that we know we have good configuration for)
        //from our Container and ask Context to give us the nonGeneric version
        var userImplementationType = Logic.Instance.GetExportedTypes<IUser>().FirstOrDefault();
        Assert.IsNotNull(userImplementationType, "We havn't been able to load TestImplementation into catalog. Please ensure the PluginsPath is set correctly at App.Config");
        var nonGeneric = dbContext.Set(userImplementationType);
        //
        // This is working so far, we can add and remove records from database using
        // the nonGeneric version of DbSet. You can uncomment the following code block provide a unique ID
        // and test it yourself.
        //
        var newUser = Logic.Instance.New<IUser>();
        newUser.Id = "99";
        newUser.UserName = "Aidin Sadighi";
        nonGeneric.Add(newUser);
        try
        {
            dbContext.SaveChanges();
        }
        catch (DbUpdateException ex)
        {
            //This is OK because most probably this is a duplicate user. Just increase the Id to make it unique.
        }



        //Failed Attempt#3: Cast non generic DbSet to generic
        try
        {
            //TODO: I need to fix this. Help me please 
            var genericSet = nonGeneric.Cast<IUser>();
        }
        catch (InvalidCastException ex)
        {
            //Cannot create a DbSet<IUser> from a non-generic DbSet for objects of type 'User'.
            throw;
        }
    }
}
Lachezar
  • 6,523
  • 3
  • 33
  • 34
Aidin
  • 2,134
  • 22
  • 26
  • How about `IQueryable generic = nonGeneric.Cast();`? – haim770 Oct 30 '13 at 20:06
  • I know jack about Entity Framework, but looking at the code i'm pretty skeptic about context.Set(). does this work: User nonGeneric = context.Set(typeof(User)); ..if not, then its normal that the cast doesn't either, and if it does, just use IUser generic = context.Set(typeof(User)); – Robert Hoffmann Oct 31 '13 at 02:03
  • Thanks for response but this doesn't work for me. I edited my question to provide a sample code and more details. Thanks – Aidin Oct 31 '13 at 20:00

4 Answers4

10

For this, I would actually suggest using reflection. In the constructor of your DbContext, you can set a property to the function pointer:

method = this.GetType().GetMethod("Set", new Type[0]).MakeGenericMethod(typeof(UserImplementation));

You can then invoke this using:

method.Invoke(this, new object[0]);

And this should return an object of type DbSet<UserImplementation> which the .Cast<>() method can then be invoked on.

kevin.groat
  • 1,274
  • 12
  • 21
  • Thanks for response but this doesn't work for me. I edited my question to provide a sample code and more details. Thanks – Aidin Oct 31 '13 at 19:58
  • I really like your approach. That is very genius. However I am getting AmbiguousMatchFound exception when calling the following line: `var method = typeof(MyContext).GetMethod("Set").MakeGenericMethod(userImplementationType);` – Aidin Nov 03 '13 at 18:12
  • Awesome, fixed it. I had to change that code a bit but your approach was the key. Thank you so much you just made my day. Correct code is: `var method = typeof(MyContext).GetMethod("Set", new Type[0]).MakeGenericMethod(userImplementationType); var genericItem = method.Invoke(dbContext, new object[0]);` – Aidin Nov 03 '13 at 18:27
  • 2
    I dont understand how to use this. I have: var set = context.Set(ImpType); and need to return IQueryable. T is IMyType and ImpType is a Type object from a class MyType : IMyType. I tried set.Cast(), but got the error that it cant create generic from non generic. – Poul K. Sørensen Nov 30 '13 at 03:15
  • 1
    @pksorensen: sorry for the extremely late reply, but if you know the implementation type at compile-time, you can simply use: `var iMyQueryable = this.Set().Cast();`. If you want it to be at run-time, then you would have to use: `var myQueryable = this.GetType().GetMethod("Set", new Type[0]).MakeGenericMethod(typeof(MyType)).Invoke(this, new [0]);` Then you would have an `IQueryable`, which you could then call `myQueryable.Cast()` to get an `IQueryable`. – kevin.groat Oct 19 '15 at 14:01
  • This returns a object of type `object`, which leaves me to cast the object into `DbSet` which in turn can't be casted... Aidin, what did you do with the object to convert it to a `DbSet`? – seebiscuit Jul 07 '17 at 17:52
  • I also don't know how they managed to get the object to DbSet cast done. But I got it working by doing a cast to IQueryable which allows you to do pretty much the same operations as the DbSet. Code: `context.GetType().GetMethod("Set", new Type[0]).MakeGenericMethod(type).Invoke(context, new object[0]) as IQueryable;` – joaoroque Nov 23 '20 at 15:16
2

replace

nonGeneric.Cast<IUser>();

by

Enumerable.Cast<IUser>(nonGeneric);
Olivier
  • 67
  • 4
0

Ok i know nothing about Entity framework but from looking at the docs

http://msdn.microsoft.com/en-us/library/gg696521%28v=vs.103%29.aspx

DbSet<TEntity> item = DbContext.Set<TEntity>;

so actually your code would be the same as this:

DbSet<User> nonGeneric = context.Set<User>();

and to get a IUser

DbSet<IUser> nonGeneric = context.Set<User>();

or maybe

var generic = nonGeneric.Cast<DbSet<IUser>>();
Robert Hoffmann
  • 2,366
  • 19
  • 29
  • Thanks for response but this doesn't work for me. I edited my question to provide a sample code and more details. Thanks – Aidin Oct 31 '13 at 19:58
0

As I wasn't able to cast a generic DbSet to a typed DbSet I used instead a typed IQueryable which can do the same things I needed from the DbSet.

Here is a extension that can get you that:

    public static IQueryable<T> GetIQueryableByTableName<T>(this DbContext context, string tableName)
    {
        var type = Assembly.GetExecutingAssembly().GetTypes().FirstOrDefault(t => t.Name == tableName);
        if (type == null)
        {
            throw new Exception("GetIQueryableByTableName received an invalid table name.");
        }
        return context.GetType().GetMethod("Set", new Type[0]).MakeGenericMethod(type).Invoke(context, new object[0]) as IQueryable<T>;
    }
joaoroque
  • 179
  • 10