Find a specified generic DbSet in a DbContext dynamically when I have an entity
Asked Answered
I

6

54

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?

Inrush answered 3/2, 2014 at 17:19 Comment(0)
S
75

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);         
}
Sajovich answered 3/2, 2014 at 17:31 Comment(4)
Is there any way to get generic DbSets also?Inrush
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<T>() method which returns the DbSet<T>.Sajovich
@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.Slipon
@Inrush - To generic dbsets you can create a switch getter and pass the context as a string to get the dbset.Hankow
P
45

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>;
} 
Phrygia answered 24/11, 2017 at 0:22 Comment(9)
Or if you want to have IQueryable<T>: 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>; }Razor
But this returns an IQueryable and not the DbSet which is needed to add to the context.Tamarau
Second method is out of scope as original Set method is already generic typed. Also,original Set method returns a DbSet<T> object. I did not understand how these extensions could be made of use.Gazo
For me this was useful, as all of my Entitys are implementing same interface, and I can then cast IQueryable to IQueryable<Interface>.Typhoeus
I am using EF Core 5 and the "Set" method is presentImpaction
@Impaction - "Set" is present, but you cannot call Set(entityToAdd.GetType()) for example, it requires a type.Thousand
@Thousand 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<T> Set<T>(this DbContext context)" is already present in DbContext). However I managed to use generics very easily, e.g. ctx.Set<T>().Add(entityToAdd) without needing to use reflection for a non generic Set.Impaction
If you are using EF Core 5.0 and up, since they've added a new overload: DbContext.Set<TEntity>(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());Thievish
And if you are using the generic version of Set<T>(), you might event don't need this reflection workaround, plz see my answer belowThievish
A
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;
    }
Amati answered 27/12, 2019 at 5:39 Comment(4)
Strangely, the error was not happening systematically. This seems to have solved it. Thanks.Tribe
It works if you want to read from DbSet but you can not use it for Add or Remove.Worn
This ambiguous only happens on EF Core 5.0 and up since they've add a overload for this methodThievish
Thanks Sam, I don't see the error any more!Sudd
I
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

Indeterminable answered 24/1, 2020 at 10:23 Comment(2)
Is it possible to get DbSets back now in 3.1 or 5.0?Peddle
Yes, it is possible to get the DbSet in 5.0 Core. I tested it with Microsoft.EntityFrameworkCore.Sqlite version 5.0.3Trundle
T
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>()
}
Thievish answered 14/2, 2022 at 23:20 Comment(5)
Doesn't answer the question.Copland
@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<SomeEntityType> to get the DbSet<SomeEntityType>, I'm just adding a hint to a generic constraint that only allows you to pass a type of class as its generic type paramThievish
Yeah, see the question, not just the title. They don't have a generic class argument.Copland
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: stackoverflow.com/a/47464834Thievish
Yep, because that's left when you only have a type instance. Slow or not.Copland
G
-5

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();
Grange answered 7/7, 2017 at 6:35 Comment(1)
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.Rubescent

© 2022 - 2024 — McMap. All rights reserved.