Question

I have following classes and DbContext:

public class Order : BaseEntity
{
    public Number {get; set;}
}
public class Product : BaseEntity;
{
    public Name {get; set;} 
}

public class Context : DbContext
{
    ....
    public DbSet<Order> Orders { set; get; }
    public DbSet<Product> Products { set; get; }
    ....
}   

I have a list of objects that want to add to my context, too, but I don't know how can I find appropriate generic DbSet according each entity type dynamically.

IList<BaseEntity> list = new List<BaseEntity>();
Order o1 = new Order();
o1.Numner = "Ord1";
list.Add(o1);

Product p1 = new Product();
p1.Name = "Pencil";
list.Add(p1);

Context cntx = new Context();  
foreach (BaseEntity entity in list)
{
      cntx.Set<?>().Add(entity);         
}

How can I do that?

Was it helpful?

Solution

DbContext has a method called Set, that you can use to get a non-generic DbSet, such as:

var someDbSet = this.Set(typeof(SomeEntity));

So in your case:

foreach (BaseEntity entity in list)
{
      cntx.Set(entity.GetType()).Add(entity);         
}

OTHER TIPS

The question does not specify EF version and the proposed answer does not work anymore for Entity Framework Core (in EF Core, DbContext does not have a non-generic Set method, at least at the date of this answer).

Yet you can still have a working extension method using Jon Skeet's answer to this question. My code is added below for convenience.

Update: Added the generic function call as well returning IQueryable<T> thanks to the comment from Shaddix.

public static IQueryable Set(this DbContext context, Type T)
{
    // Get the generic type definition
    MethodInfo method = typeof(DbContext).GetMethod(nameof(DbContext.Set), BindingFlags.Public | BindingFlags.Instance);

    // Build a method with the specific type argument you're interested in
    method = method.MakeGenericMethod(T);

    return method.Invoke(context, null) as IQueryable;
}

public static IQueryable<T> Set<T>(this DbContext context)
{
    // Get the generic type definition 
    MethodInfo method = typeof(DbContext).GetMethod(nameof(DbContext.Set), BindingFlags.Public | BindingFlags.Instance);

    // Build a method with the specific type argument you're interested in 
    method = method.MakeGenericMethod(typeof(T)); 

    return method.Invoke(context, null) as IQueryable<T>;
} 

To avoid error "System.Reflection.AmbiguousMatchException: 'Ambiguous match found.'" I used version below:

    public static IQueryable Set(this DbContext context, Type T)
    {
        var method = typeof(DbContext).GetMethods().Single(p =>
            p.Name == nameof(DbContext.Set) && p.ContainsGenericParameters && !p.GetParameters().Any());
                               
        // Build a method with the specific type argument you're interested in
        method = method.MakeGenericMethod(T);

        return method.Invoke(context, null) as IQueryable;
    }

Unfortunately, the below proposed version does not work since .NET Core 3.0. You still can get an IQueryable back, but you cannot cast it to DbSet anymore.

IQueryable<TEntity> as DbSet<TEntity> => null

What's worse is that starting with EF Core 3.0, the new FromSqlRaw and FromSqlInterpolated methods (which replace FromSql) can only be specified on query roots, i.e. directly on the DbSet<> and not on IQueryable. Attempting to specify them anywhere else will result in a compilation error.

See https://github.com/dotnet/efcore/issues/15704#issuecomment-493230352

In EF Core we only have Set<TEntity>() method(s) which have a constraint that TEntity must be a type of class, so this prevents you accidentally passing a interface type that EF is impossible to find out any existing DbSet.

So if it's possible, you can always restrict your generic type parameter as the same as Set<TEntity>() requiring, for example this will not work:

public void GetDbSet<T>(DbContext db) {
    db.Set<T>()
//     ~~~~~~~~
// Error CS0452 The type 'T' must be a reference type in order to use it as parameter 'TEntity' in the generic type or method 'DbContext.Set<TEntity>()'
}

but this will:

public void GetDbSet<T>(DbContext db) where T : class {
    db.Set<T>()
}

In my case, I wanted a dynamic function that accepted a DbSet<T> (type of database class that you want to pass as a parameter) and a specific expression to build a query on that table. An easy way to do it would be like this:

private IQueryable<T> BuildQueriedCollection<T>(Expression<Func<T, bool>> exp, DbSet<T> dbTable) where T : class
{
    var appliedQueryCollection = dbTable.AsExpandable().Where(exp);
    return appliedQueryCollection;
}

and you could call the function like so:

Expression<Func<MyClass, bool>> myExp = myList => myList... // some condition...;
var dbset = dbContext.MyTable;
var query = BuildQueriedCollection(myExp, dbset);

In addition to Pablo's answer, you can add a class to your project:

namespace System.Data.Entity
{
    public static class EntityFrameworkExtensions
    {
        public static IEnumerable<object> AsEnumerable(this DbSet set)
        {
            foreach (var entity in set)
            {
                yield return entity;
            }
        }
    }
}

This class adds an extention method AsEnumerable to DbSet instance.

When you want to use it, for example to Count or filter a row:

var someDbSet = this.Set(typeof(SomeEntity));
var count = (from a in someDbSet.AsEnumerable() select a).Count();
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top