Find a generic DbSet in a DbContext dynamically
Asked Answered
R

4

11

I know this question has already been asked but I couldn't find an answer that satisfied me. What I am trying to do is to retrieve a particular DbSet<T> based on its type's name.

I have the following :

[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("MyDllAssemblyName")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("MyCallingAssemblyName")]

class MyDbContext : DbContext {

    public DbSet<ModelA> A { get; set; }
    public DbSet<ModelB> B { get; set; }

    public dynamic GetByName_SwitchTest(string name) {
        switch (name) {
            case "A": return A;
            case "B": return B;
        }
    }

    public dynamic GetByName_ReflectionTest(string fullname)
    {
        Type targetType = Type.GetType(fullname);
        var model = GetType()
            .GetRuntimeProperties()
            .Where(o => 
                o.PropertyType.IsGenericType &&
                o.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>) &&
                o.PropertyType.GenericTypeArguments.Contains(targetType))
            .FirstOrDefault();
        if (null != model)
            return model.GetValue(this);
        return null;
    }
}

I have no trouble getting the type itself whether it is via a simple switch or reflection. I need however to return the type as a dynamic since I do not know what DbSet type it will be. Then somewhere else in the same assembly, I use it this way :

// MyDbContext MyDbContextInstance..
var model = MyDbContextInstance.GetByName_SwitchTest("A");
var record1 = model.FirstOrDefault(); // It crashes here with RunTimeBinderException

At this point model contains an instance of a InternalDbSet<ModelA> type. From there, any use I do with the model object I get a RunTimeBinderException : 'Microsoft.Data.Entity.Internal.InternalDbSet' does not contain a definition for 'FirstOrDefault'

Investigating on the web, I found a blog post explaining that (dixit his blog) :

the reason the call to FirstOrDefault() fails is that the type information of model is not available at runtime. The reason it's not available is because anonymous types are not public. When the method is returning an instance of that anonymous type, it's returning a System.Object which references an instance of an anonymous type - a type whose info isn't available to the main program.

And then he points that a solution :

The solution is actually quite simple. All we have to do is open up AssemplyInfo.cs of the ClassLibrary1 project and add the following line to it: [assembly:InternalsVisibleTo("assembly-name")]

I did try this solution on my code but it doesn't work. For info I have an asp.net 5 solution with two assemblies running on dnx dotnet46. An app and a dll containing all my models and DbContext. All the concerned calls I do are located on the dll though.

Does this solution have any chance to work ? Am I missing something ? Any pointers would be greatly appreciated ?

Thanks in advance

[EDIT]

I have tried to return IQueryable<dynamic> rather than dynamic and I could do the basic query model.FirstOrDefault(); but above all I'd like to be able to filter on a field too :

var record = model.FirstOrDefault(item => item.MyProperty == true);
Rathe answered 26/11, 2015 at 13:56 Comment(5)
how about change the dynamic to IEnumerable<dynamic> ?Trawl
I did try something similar ( changed to IQueryable<dynamic> ) and even though I could do some queries like model.FirstOrDefault() it won't allow me to do things like model.FirstOrDefault(item => item.MyProperty == true). which reduces the usefulness of the thing.Rathe
I have run into this same issue. Any chance that you have reached a resolution?Freelance
Hi @Asryael, unfortunately I haven't found a proper solution yet and by lack of time I had to come up with an alternative solution that works ok in my case scenario ( Posted below ). Hopefully this can help you out.Rathe
Does this not work var mydbset= this.Set(typeof(SomeEntity));Storied
R
0

*Disclaimer: This response doesn't give a stricto sensu answer to my question. It is rather a different approach to resolve my own problem. I am aware this is a specific example for a given situation that will not work for everyone. I am posting this approach in the hope it helps someone but will not mark it as the answer as I am still hoping for a real solution.

To start with, let's accept the fact that the only useful information we can get out of the current code is whether a record exists or not.. Any attempt of a dynamic queries after that would give the RuntimeBinderException.

Then let's continue with another fact; DbContext.Add(object) and DbContext.Update(object) are not template based so we can use them to save our models ( Instead of db.A.Add() or db.A.Update() )

In my own situation, no more is required to work out a procedure

  1. Define models a little differently

To start with, I need a field that is retrievable across all my models which should obviously be a way to identify a unique record.

// IModel give me a reliable common field to all my models ( Fits my DB design maybe not yours though )
interface IModel { Guid Id { get; set; } }

// ModelA inherit IModel so that I always have access to an 'Id'
class ModelA : IModel {
    public Guid Id { get; set; }
    public int OtherField { get; set; }
}

// ModelB inherit IModel so that I always have access to an 'Id'
class ModelB : IModel {
    public Guid Id { get; set; }
    public string WhateverOtherField { get; set; }
}
  1. Re-purpose the dynamic queries a bit to do something we know works

I haven't found a way to do smart query dynamically, so instead I know I can reliably identify a record and know if it exists or not.

class MyDbContext : DbContext {

    public DbSet<ModelA> A { get; set; }
    public DbSet<ModelB> B { get; set; }

    // In my case, this method help me to know the next action I need to do
    // The switch/case option is not pretty but might have better performance 
    // than Reflection. Anyhow, this is one's choice.
    public bool HasRecord_SwitchTest(string name) {
        switch (name) {
            case "A": return A.AsNoTracking().Any(o => o.Id == id);
            case "B": return B.AsNoTracking().Any(o => o.Id == id);
        }
        return false;
    }

    // In my case, this method help me to know the next action I need to do
    public bool HasRecord_ReflectionTest(string fullname)
    {
        Type targetType = Type.GetType(fullname);
        var model = GetType()
            .GetRuntimeProperties()
            .Where(o => 
                o.PropertyType.IsGenericType &&
                o.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>) &&
                o.PropertyType.GenericTypeArguments.Contains(targetType))
            .FirstOrDefault();
        if (null != model)
            return (bool)model.GetValue(this).AsNoTracking().Any(o => o.Id == id);
        return false;
    }

    // Update and save immediately - simplified for example
    public async Task<bool> UpdateDynamic(object content)
    {
        EntityEntry entry = Update(content, GraphBehavior.SingleObject);
        return 1 == await SaveChangesAsync(true);
    }

    // Insert and save immediately - simplified for example
    public async Task<bool> InsertDynamic(object content)
    {
        EntityEntry entry = Add(content, GraphBehavior.SingleObject);
        return 1 == await SaveChangesAsync(true);
    }
}
  1. A little bit of plumbing to give a sense to my situation

Next, what I needed to do with that dynamic queries was a way to replicate data from a server down to my client. ( I have omitted a big chunk of the architecture to simplify this example )

class ReplicationItem
{
    public ReplicationAction Action { get; set; } // = Create, Update, Delete
    public string ModelName { get; set; } // Model name
    public Guid Id { get; set; } // Unique identified across whole platform
}
  1. Connecting the bits.

Now, here's the routine that connects the bits

public async void ProcessReplicationItem(ReplicationItem replicationItem)
{
    using (var db = new MyDbContext())
    {
        // Custom method that attempts to get remote value by Model Name and Id
        // This is where I get the strongly typed object 
        var remoteRecord = await TryGetAsync(replicationItem.ModelName, replicationItem.Id);
        bool hasRemoteRecord = remoteRecord.Content != null;

        // Get to know if a local copy of this record exists.
        bool hasLocalRecord = db.HasRecord_ReflectionTest(replicationItem.ModelName, replicationItem.Id);

        // Ensure response is valid whether it is a successful get or error is meaningful ( ie. NotFound )
        if (remoteRecord.Success || remoteRecord.ResponseCode == System.Net.HttpStatusCode.NotFound)
        {
            switch (replicationItem.Action)
            {
                case ReplicationAction.Create:
                {
                    if (hasRemoteRecord)
                    {
                        if (hasLocalRecord)
                            await db.UpdateDynamic(remoteRecord.Content);
                        else
                            await db.InsertDynamic(remoteRecord.Content);
                    }
                    // else - Do nothing
                    break;
                }
                case ReplicationAction.Update:
                    [etc...]
            }
        }
    }
}

// Get record from server and with 'response.Content.ReadAsAsync' type it 
// already to the appropriately
public static async Task<Response> TryGetAsync(ReplicationItem item)
{
    if (string.IsNullOrWhiteSpace(item.ModelName))
    {
        throw new ArgumentException("Missing a model name", nameof(item));
    }

    if (item.Id == Guid.Empty)
    {
        throw new ArgumentException("Missing a primary key", nameof(item));
    }

    // This black box, just extrapolate a uri based on model name and id
    // typically "api/ModelA/{the-guid}"
    string uri = GetPathFromMessage(item);

    using (var client = new HttpClient())
    {
        client.BaseAddress = new Uri("http://localhost:12345");

        HttpResponseMessage response = await client.GetAsync(uri);
        if (response.IsSuccessStatusCode)
        {
            return new Response()
            {
                Content = await response.Content.ReadAsAsync(Type.GetType(item.ModelName)),
                Success = true,
                ResponseCode = response.StatusCode
            };
        }
        else
        {
            return new Response()
            {
                Success = false,
                ResponseCode = response.StatusCode
            };
        }
    }
}

public class Response
{
    public object Content { get; set; }
    public bool Success { get; set; }
    public HttpStatusCode ResponseCode { get; set; }
}

ps: I am still interested in a real answer, so please keep posting for other answer if you have a real one to share.

Rathe answered 26/11, 2015 at 14:26 Comment(0)
S
0

So how did I do it when I am not aware of <T> during compile time.

First need to get the type as DbContext.Set method returns a non-generic DbSet instance for access to entities of the given type in the context and the underlying store.

public virtual DbSet Set(Type entityType)

Note here argument is the type of entity for which a set should be returned.And set for the given entity type is the return value.

var type = Assembly.GetExecutingAssembly().GetTypes().FirstOrDefault(t => t.Name == <Pass your table name>);

now once I have this type

if(type != null)
{
DbSet context = context.Set(type);
}

Or a one liner would be

DbSet mySet = context.Set(Type.GetType("<Your Entity Name>"));
Storied answered 14/12, 2015 at 15:36 Comment(1)
Thank you for your answer. However - public virtual DbSet Set(Type entityType) - seems to no longer exist with Entity Framework 7. On top on that, DbSet wouldn't allow me to do fluent API queries on the models.Rathe
K
0

You could use this to get the DBSet for a specific type:

public object GetByType(DbContextcontext, Type type) {
     var methode = _context.GetType().GetMethod("Set", types: Type.EmptyTypes);
     if (methode == null) {
        return null;
     }
     return methode.MakeGenericMethod(type).Invoke(_context, null);
}
        
Koeninger answered 24/1, 2023 at 11:24 Comment(1)
You can extract the MethodInfo from a delegate instead of using reflection; new Func<DbSet<object>>(_context.Set<object>).Method.GetGenericMethodDefinition(). Which is a useful trick when a type has multiple overloads of the same method name.Fosterfosterage
J
0

Instead of using reflection or a switch to get the DBContext from the properties if you alredy know the type you want to access I recommend using Context.Set<ModelA>() I would also recommend using generic types instead of using reflection, it would work best if ModelA and ModelB inherited from a base abstract class

public class Something<ModelT> : where ModelT : BaseModel
{
  public void DoSomething(Context context){

    ModelT? model = Context.Set<ModelT>().FirstOrDefault();
    // you can cast it here or create a child class for each type of model 
    // cast example:
    if(model is ModelA modela){
      // Code for A

    }else if(model is ModelB modelb){
      // Code for B
    }



  }

}
Jin answered 24/6 at 6:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.