52

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?

Lucius
  • 2,794
  • 4
  • 20
  • 42
Masoud
  • 8,020
  • 12
  • 62
  • 123

6 Answers6

70

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);         
}
Pablo Romeo
  • 11,298
  • 2
  • 30
  • 58
  • Is there any way to get generic DbSets also? – Masoud Feb 04 '14 at 06:14
  • 13
    I'm not sure how you would plan to use that, given that you only know the base type at compile time, but FYI, there is also a Set() method which returns the DbSet. – Pablo Romeo Feb 04 '14 at 07:03
  • @PabloRomeo You can use it in a way that I'm about to. In a abstract class that expects derived classes to define their type. – Jesse Feb 16 '18 at 04:14
  • @Masoud - To generic dbsets you can create a switch getter and pass the context as a string to get the dbset. – LizardKingLK Jan 17 '22 at 14:19
44

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>;
} 
CarenRose
  • 1,266
  • 1
  • 12
  • 24
user3141326
  • 1,423
  • 2
  • 21
  • 31
  • 1
    Or if you want to have IQueryable: public static IQueryable Set(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; } – Shaddix Dec 06 '17 at 07:27
  • 14
    But this returns an IQueryable and not the DbSet which is needed to add to the context. – Miles Apr 25 '19 at 17:23
  • 7
    Second method is out of scope as original Set method is already generic typed. Also,original Set method returns a DbSet object. I did not understand how these extensions could be made of use. – ibubi Sep 10 '19 at 11:49
  • For me this was useful, as all of my Entitys are implementing same interface, and I can then cast IQueryable to IQueryable. – user3777939 Dec 11 '19 at 07:47
  • I am using EF Core 5 and the "Set" method is present – fededim Nov 25 '20 at 17:16
  • @fededim - "Set" is present, but you cannot call Set(entityToAdd.GetType()) for example, it requires a type. – Mike Jan 22 '21 at 20:53
  • @Mike Ok I missed "DbContext does not have a non-generic Set method", now I understand the answer (even though the second method "public static IQueryable Set(this DbContext context)" is already present in DbContext). However I managed to use generics very easily, e.g. ctx.Set().Add(entityToAdd) without needing to use reflection for a non generic Set. – fededim Jan 24 '21 at 10:29
  • If you are using EF Core 5.0 and up, since they've [added a new overload](https://github.com/dotnet/efcore/issues/23683): `DbContext.Set(string)`, you will have to modify this line: `var method = typeof(DbContext).GetMethod(nameof(DbContext.Set), BindingFlags.Public | BindingFlags.Instance);` to `var method = typeof(DbContext).GetMethods(BindingFlags.Public | BindingFlags.Instance).First(m => m.Name == nameof(DbContext.Set) && !m.GetParameters().Any());` – n0099 Feb 14 '22 at 14:47
  • And if you are using the generic version of `Set()`, you might event don't need this reflection workaround, plz see [my answer below](https://stackoverflow.com/a/71119399) – n0099 Feb 14 '22 at 23:25
10

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;
    }
Community
  • 1
  • 1
sam sergiy klok
  • 526
  • 7
  • 17
5

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

Poiser
  • 101
  • 2
  • 6
2

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>()
}
n0099
  • 520
  • 1
  • 4
  • 12
  • Doesn't answer the question. – Gert Arnold Sep 29 '22 at 06:39
  • @GertArnold the op want to know `how can I find appropriate generic DbSet according each entity type dynamically`, if he's using EFCore then he can just invokes the `dbContext.Set` to get the `DbSet`, I'm just adding a hint to a generic constraint that only allows you to pass a type of class as its generic type param – n0099 Sep 29 '22 at 16:20
  • Yeah, see the question, not just the title. They don't have a generic class argument. – Gert Arnold Sep 29 '22 at 17:10
  • Also other answers doesn't update with the fact that the generic variant of method dbContext.Set() is introduced in EF Core 5.0, they've still using slowly runtime reflection to achieve this: https://stackoverflow.com/a/47464834 – n0099 Sep 29 '22 at 19:08
  • Yep, because that's left when you only have a type instance. Slow or not. – Gert Arnold Sep 29 '22 at 19:29
-4

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();
CarenRose
  • 1,266
  • 1
  • 12
  • 24
Ali
  • 3,373
  • 5
  • 42
  • 54
  • 4
    Downvoted because I can't see what this does. DbSet is already IQueryable which extends IEnumerable, and converting it to IEnumerable by iterating all elements is not only nonsense, but also not performant. – vincent163 Oct 12 '18 at 05:14