5

I'm trying to use System.Reflections to get a DbSet<T> dynamically from its name.

What I've got right now is:

  • The DbSet name
  • The DbSet's Type stored on a variable

The issue I'm facing comes out when trying to use the dbcontext.Set<T>() method, since (these are my tries so far):

  • When I try to assign to <T> my DbSet Type, it throws me the following compilation error:

    "XXX is a variable but is used like a type"

  • If I try with using both the Extension methods that you will find below in my code (which I made in order to try to get an IQueryable<T>), it returns a IQueryable<object>, which unfortunately is not what I am looking for, since of course when I try to manipulate it with further Reflections, it lacks of all the properties that the original class has…

What am I doing wrong? How can I get a DbSet<T>?

My code is the following, but of course, let me know if you need more infos, clarifications or code snippets.

My Controller's Method:

public bool MyMethod (string t, int id, string jsonupdate)
{
    string _tableName = t;
    Type _type = TypeFinder.FindType(_tableName); //returns the correct type

    //FIRST TRY
    //throws error: "_type is a variable but is used like a type"
    var tableSet = _context.Set<_type>();  

    //SECOND TRY
    //returns me an IQueryable<object>, I need an IQueryable<MyType>
    var tableSet2 = _context.Set(_type);  

    //THIRD TRY
    //always returns me am IQueryable<object>, I need an IQueryable<MyType>
    var calcInstance = Activator.CreateInstance(_type);
    var _tableSet3 = _context.Set2(calcInstance);

    //...
}

Class ContextSetExtension

public static class ContextSetExtension
{ 
    public static IQueryable<object> Set(this DbContext _context, Type t)
    {
        var res= _context.GetType().GetMethod("Set").MakeGenericMethod(t).Invoke(_context, null);
        return (IQueryable<object>)res;
    }


    public static IQueryable<T>Set2<T>(this DbContext _context, T t) 
    {
        var typo = t.GetType();
        return (IQueryable<T>)_context.GetType().GetMethod("Set").MakeGenericMethod(typo).Invoke(_context, null);
    }
}

EDIT Added TypeFinder's inner code.
In brief, this method does the same of Type.GetType, but searches Type on ALL the generated assemblies

public class TypeFinder
{

    public TypeFinder()
    {
    }
    public static Type FindType(string name)
    {


        Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
        var result = (from elem in (from app in assemblies
                                        select (from tip in app.GetTypes()
                                                where tip.Name == name.Trim()
                                                select tip).FirstOrDefault())
                      where elem != null
                      select elem).FirstOrDefault();

        return result;
    }
}

UPDATE as requested in the comments, here's the specific case:

In my DB i've got some tables which are really similar each other, so the idea was to create a dynamic table-update method which would be good for every table, just passing to this method the table name, the ID of the row to update and the JSON containing data to update.
So, in brief, I would perform some updates on the table given in input as DbSet type, updating the row with ID==id in input with the data contained inside the JSON, which will be parsed inside an object of type X(the same of dbset)/into a dictionary.

In pseudo-code:

public bool MyMethod (string t, int id, string jsonupdate)
{
    string _tableName = t;
    Type _type = TypeFinder.FindType(_tableName); //returns the correct type

    //THIS DOESN'T WORKS, of course, since as said above:
    //<<throws error: "_type is a variable but is used like a type">>
    var tableSet = _context.Set<_type>();  

    //parsing the JSON
    var newObj = Newtonsoft.Json.JsonConvert.DeserializeObject(jsonupdate, _type);

    //THIS OF COURSE DOESN'T WORKS TOO
    //selecting the row to update:
    var toUpdate = tableSet.Where(x => x.Id == id).FirstOrDefault();

    if(toUpdate!=null)
    {

       var newProperties = newObj.GetType().GetProperties();
       var toUpdateProperties = toUpdate.GetType().GetProperties();

       foreach(var item in properties)
       {
           var temp = toUpdateProperties.Where(p => p.Name==item.Name)
           {
              //I write it really in briefand fast, without lots of checks.
              //I think this is enough, I hope
              temp.SetValue(toUpdate, item.GetValue());
           }
       }

       _context.SaveChanges();
    }

    return false;
}
Santa Cloud
  • 547
  • 1
  • 10
  • 22
  • What is the problem you are actually trying to solve? Why specifically start from a string? Are you able to start from a generic type instead? – Flater Oct 09 '18 at 13:03
  • Hi @Flater, I'll answert in order! I am trying to do this: `_context.Set();`, by having `` stored in a variable of type `Type`. I'm starting from a string since its a web api controller method which - in front of the name of the table that the front end is requiring - returns me the db table content. And unfortunately, I am not able to start from a generic type – Santa Cloud Oct 09 '18 at 13:07
  • Possible duplicate of [How do I use reflection to call a generic method?](https://stackoverflow.com/questions/232535/how-do-i-use-reflection-to-call-a-generic-method) – Flater Oct 09 '18 at 13:29
  • 1
    @Flater it's similar but not a duplicate. I gave a look to that question and its answers just yesterday, it's a really good one but does not cover my issues (issues that, as says nvoigt in his answer, sadly are impossible to solve in this way I intended and the only thing to do is to change my general approach) – Santa Cloud Oct 09 '18 at 13:57
  • 1
    Let assume you have a method `public bool MyMethod(int id, string jsonupdate) where T : class`. What would you do inside that method? – Ivan Stoev Oct 09 '18 at 19:12
  • 2
    Because w/o some specific use case, your question is duplicate of [Dynamically access table in EF Core 2.0](https://stackoverflow.com/questions/48041821/dynamically-access-table-in-ef-core-2-0). You can get `IQueryable` or `IQueryable`, but then you can't do anything with it, except using eventually [Dynamic LINQ](https://learn.microsoft.com/en-us/ef/core/extensions/#microsoftentityframeworkcoredynamiclinq), untyped `DbContext` methods, reflection, manual expression tree building etc. So show your use case with (pseudo) code and we'll give you the best possible approach. – Ivan Stoev Oct 09 '18 at 19:23
  • @IvanStoev about your first comment: in my DB i've got some tables which are really similar each other, so the idea was to create a dynamic table-update method which would be good for every table. So, in brief, I would perform some updates on the table given in input as DbSet type, updating the row with ID== id in input with the data contained inside the JSON, which will be parsed inside an object of type X(the same of dbset)/into a dictionary – Santa Cloud Oct 10 '18 at 06:50

3 Answers3

1
returns me an IQueryable<object>, I need an IQueryable<MyType>

Well, that will never work. Your IQueryable cannot be of type IQueryable<MyType>because that would mean the compiler would need to know what MyType is and that is not possible, because the whole point of this exercise is to decide that on runtime.

Maybe it's enough to know that those objects are in fact instances of MyType?

If not, I think you have painted yourself into a corner here and you are trying to figure out what paint to use to get out of there. Take a step back, it's probably not a technical problem. Why do you need to do this? Why do you have the conflicting needs of knowing the type at runtime only and knowing it at compile time?

You need to think about your requirements, not about the technical details.

nvoigt
  • 75,013
  • 26
  • 93
  • 142
  • Hi, thansk for your answer! To answer to your question: `Why do you need to do this?`, the situation is that now I receive from my front end the name of the table, the ID of the row that I have to edit and a JSON with the new data to update on that row. Since I've got lots of very similar tables on my DB, I'm trying to create an unique manager (for optimization and mantainability purposes) to manage different dbsets to execute updates… So… Is it impossible to do this? Have I to statically manage a different method for each table? – Santa Cloud Oct 09 '18 at 13:48
  • 1
    It's certainly possible, but you don't need an `IQueryable` when you will set the values via reflection anyway. – nvoigt Oct 09 '18 at 14:29
  • But without having the generic type instead of Object, I can't search, for example, the element which has "XXXX" as ID without facing the error that "type object has no ID property", even if at the runtime it's representing an Entity which has the - in this case - ID property… I don't know if I've explained well… – Santa Cloud Oct 09 '18 at 14:46
  • 1
    How will you know that the entity has an Id property? Could you define a common interface of all your entity types? – nvoigt Oct 09 '18 at 15:02
  • Yes, actually in this project I can solve this with an Interface and this could likely work, but I'm keeping it as a last resort in case that is not possible to solve this by somehow casting/declaring the dbset to/as a dynamic generic type – Santa Cloud Oct 09 '18 at 15:06
0

I needed to dynamically load a single record from the database for each type in a list of known types, to print a test email when an admin is editing the template, so I did this:

List<object> args = new List<object>();

//...
//other stuff happens that isn't relevant to the OP, including adding a couple fixed items to args
//...

foreach (Type type in EmailSender.GetParameterTypes())
{
    //skip anything already in the list
    if (args.Any(a => a.GetType().IsAssignableFrom(type))) continue;

    //dynamically get an item from the database for this type, safely assume that 1st column is the PK
    string sql = dbContext.Set(type).Sql.Replace("SELECT", "SELECT TOP 1") + " ORDER BY 1 DESC";
    var biff = dbContext.Set(type).SqlQuery(sql).AsNoTracking().ToListAsync().Result.First();
    args.Add(biff);
}

Caveat: I know at least one record will exist for all entities I'm doing this for, and only one instance of each type may be passed to the email generator (which has a number of Debug.Asserts to test validity of implementation).

If you know the record ID you're looking for, rather than the entire table, you can use dbContext.Set(type).Find(). If you want the entire table of whatever type you've sussed out, you can just do this:

string sql = dbContext.Set(type).Sql; //append a WHERE clause here if needed/feasible, use reflection?
var biff = dbContext.Set(type).SqlQuery(sql).ToListAsync().Result;

Feels a little clunky, but it works. There is strangely no ToList without Async, but I can run synchronously here. In my case, it was essential to turn off Proxy Creation, but you look like you want to maintain a contextful state so you can write back to db. I'm doing a bunch of reflection later, so I don't really care about strong typing such a resulting collection (hence a List<object>). But once you have the collection (even just as object), you should be able to use System.Reflection as you are doing in your UPDATE sample code, since you know the type and can use SetValue with known/given property names in such a manner.

And I'm using .NET Framework, but hopefully this may translate over to .NET Core.

Tim
  • 169
  • 9
-1

EDIT: tested and working:

public async Task<bool> MyMethod(string _type)
{
    Type type = Type.GetType(_type);

    var tableSet = _context.Set(type);

    var list = await db.ToListAsync();

    // do something
}

// pass the full namespace of class
var result = await MyMethod("Namespace.Models.MyClass")

IMPORTANT NOTE: your DbContext need to have the DbSet declared to work!

public class MyContext : DbContext
{
    public DbSet<MyClass> MyClasses { get; set; }
}
Charles Cavalcante
  • 1,572
  • 2
  • 14
  • 27
  • Hi, I've tried but unfortunately I still get the `type is a variable but is used like a type` error on `var dbSet = _context.Set();` – Santa Cloud Oct 09 '18 at 13:17
  • I´ve edited the answer, working with Type type = Type.GetType(_type); and var tableSet = _context.Set(type); – Charles Cavalcante Oct 09 '18 at 13:24
  • Looking at the code, the `.Set(type)` of your edit is using the `Set` Extension, am I right? Because, if it's using the Set Extension, the issue is that it returns a `IQueryable`. This works fine if I need to return it `ToList()` or something "generic", but it's the same situation of my tries #2 & #3. (PS: in order to avoid misunderstandings, I am not the one who downvoted your answer) – Santa Cloud Oct 09 '18 at 13:33
  • @CharlesCavalcante is there a way to get the name of the `DbSet` property in the `DbContext`? – Shimmy Weitzhandler Aug 11 '19 at 07:59
  • 1
    No longer works in .Net Core unfortunately _context.Set(type) can no longer be used. – MartinS Feb 24 '20 at 15:50