2

I have a question. I have a dbContext that has 200+ classes that represent all the tables in the database. All the tables follow the same format. Is it possible to dynamically change the code at runtime in the following way?

var coffeeList = new ObservableCollection<GenericCoffeeList>();
var query = (from c in ctxCoin.Coffee1
             select new GenericCoffeeList { CoffeeCatalogId = c.Id, Name = c.Name, Type = c.Type })
             .ToList();

foreach (var c in query)
{
    coinList.Add(c);
}

Here is the next entity type that is almost the same. The only change is the entity

var coffeeList = new ObservableCollection<GenericCoffeeList>();

var query = (from c in ctxCoin.Coffee2
             select new GenericCoffeeList { CoffeeCatalogId = c.Id, Name = c.Name, Type = c.Type })
             .ToList();

foreach (var c in query)
{
    coinList.Add(c);
}

Is there a way to change the entity at runtime or will I have to code each entity? Thanks for any direction on this question.

Slugart
  • 4,535
  • 24
  • 32
Bryan K
  • 203
  • 1
  • 2
  • 7
  • *All the tables follow the same format* -- Do you mean that all tables have exactly the same columns? – Gert Arnold Dec 14 '21 at 11:35
  • Gert, No they are not the same amount of columns in each table. – Bryan K Dec 15 '21 at 01:19
  • On the first line of code you say **coffee**List and the last line you say **coin**List. Is it correct? – Caius Jard Dec 20 '21 at 07:27
  • Also, sounds to me like you're trying to put a C# fudge in for the fact that your database has a design flaw. You shouldn't have 200 identical tables with Id/Name/Type, you should have 1 table with Id/Name/Type/TableName columns (or some better names) - the main point being that you're storing in a table name data that should be stored in a column. – Caius Jard Dec 20 '21 at 07:30
  • Genuinely, I think I'd rework al the data into one table, with a discriminator column as to where it came from. If other software still expects to find separate tables, you can have views like `CREATE VIEW Coffee1 AS SELECT ID, Name Type FROM MainTable WHERE TableName = 'Coffee1'` - can use insteadof triggers if they need to be writable but getting all this data into one table and using it from there would resolve this headache( and probably many others). You can keep the crazy 200 classes in c# if you want and do a TPH inheritance but I'd dump it all and just have 1 entity – Caius Jard Dec 20 '21 at 07:36

4 Answers4

0

This should work for EF6 but I was testing on EFCore.

I've done something somewhat similar to this when I needed to modify all the DbSets that implement a specific interface.

You have two options for your situation. Since all of your model types don't have a common base type (other than object), you could refactor those generated classes to extract the common properties to a base class (the Id, Name, and Type properties).

Here I'm assuming your Coffee1 and Coffee2 tables just have the entity type Coffee1 and Coffee2 respectively.

Option 1:

Refactor classes and extract those common properties

public partial class Coffee1 : BaseCoffee {}
public partial class Coffee2 : BaseCoffee {}
public abstract class BaseCoffee
{
  public int Id { get; set; }
  public string Name { get; set; }
  public string Type { get; set; }
}

then your query would look like this

var dbSets = ctxCoin.GetType().GetProperties()
    .Where(p => p.PropertyType.IsAssignableTo(typeof(IQueryable<BaseCoffee>)))
    .Select(set => set.GetValue(ctxCoin))
    .Cast<IQueryable<BaseCoffee>>();

var coffeeList = new ObservableCollection<GenericCoffeeList>();

foreach (var coffee in dbSets)
{
    var query = coffee.Select(c => new GenericCoffeeList { CoffeeCatalogId = c.Id, Name = c.Name, Type = c.Type });

    foreach (var c in query)
    {
        coffeeList.Add(c);
    }
}

This option is much more type-safe and less error-prone.

Option 2:

Use reflection on IQueryable<object>

Keep your models the same and use this:

var dbSets = ctxCoin.GetType().GetProperties()
    .Where(p => p.PropertyType.IsAssignableTo(typeof(IQueryable<object>)))
    .Select(set => set.GetValue(ctxCoin))
    .Cast<IQueryable<object>>();

var coffeeList = new ObservableCollection<GenericCoffeeList>();

foreach (var queryableObject in dbSets)
{
    var query = queryableObject.Select(GenerateGenericCoffee);

    foreach (var c in query)
    {
        coffeeList.Add(c);
    }
}

The GenerateGenericCoffee helper method

GenericCoffeeList GenerateGenericCoffee(object coffeeObject)
{
    var objType = coffeeObject.GetType();

    return new GenericCoffeeList
    {
        CoffeeCatalogId = GetProperty<int>("Id"),
        Name = GetProperty<string>("Name"),
        Type = GetProperty<string>("Type"),
    };

    T GetProperty<T>(string name)
    {
        return (T)objType.GetProperty(name).GetValue(coffeeObject);
    }
}

If all of your models contain Id, Name, and Type, you will be fine otherwise you'll need to check those properties exist first before making the GenericCoffeeList item.

Hank
  • 1,976
  • 10
  • 15
  • Hank, Thanks for the code. Question where are you getting the BaseCoffee? – Bryan K Nov 17 '21 at 16:15
  • The DbSets on your context ctxCoin, do all the type arguments to those have the same base class or they just happen to have the same properties? i.e. You have `DbSet Coffee1 { get; set; }` and `DbSet Coffee2 { get; set; }`, and Class1 and Class2 both have the base class `BaseCoffee` with the properties `Id`, `Name`, and `Type`. – Hank Nov 17 '21 at 17:49
  • Hank, I used enitity framework to generate the class files they do not share a base class. The framework creates a dbContext called CoffeeCatalogContext. I use it to access the partial classes created by the framework. I’m also using EF Core – Bryan K Nov 17 '21 at 22:18
  • @BryanK I've rewritten my answer to give you more options that reflect your situation. Hopefully, this helps! – Hank Nov 18 '21 at 00:49
  • Hank. Thank for the code. I do not have the methos IsAssignableTo. My app is blazor and c# entity framework core. From what I can tell the IsAssignableTo is a .net method. – Bryan K Nov 18 '21 at 01:54
  • @BryanK It's a newer .NET 5 and 6 convenience method. You can use its twin `typeof(IQueryable).IsAssignableFrom(p.PropertyType)` instead. This one has been available since .NETFramework. – Hank Nov 18 '21 at 03:19
  • I’ll give it a try. Thanks for all your help. – Bryan K Nov 18 '21 at 23:10
  • Hank, I'm not getting any results from this part of the code and cant figure out why. var dbSets = ctxCoin.GetType().GetProperties() .Where(p => p.PropertyType.IsAssignableFrom(typeof(IQueryable))) .Select(set => set.GetValue(ctxCoin)) .Cast>(); – Bryan K Nov 20 '21 at 23:18
  • You have the reflection call backward. If you have an abstract base class `A` and you want to test if `B` inherits that class you either call `typeof(A).IsAssignableFrom(typeof(B))` or `typeof(B).IsAssignableTo(typeof(A)`. Look at the order of the types in my comment and you'll see your mistake. – Hank Nov 21 '21 at 01:35
  • Hank, Sorry for not getting it. I have posted the code I'm trying to use. What am I doing wrong? – Bryan K Nov 21 '21 at 02:08
  • The part filtering the properties is backwards like I had stated earlier. `.Where(p => p.PropertyType.IsAssignableFrom(typeof(IQueryable)))` needs to be `.Where(p => typeof(IQueryable).IsAssignableFrom(p.PropertyType))`. Look at the ordering of the types `typeof(IQueryable)` and `p.PropertyType`. – Hank Nov 21 '21 at 03:43
  • I got the code to work making the change you suggested. Thanks. But I do not see how it helps with my initial question to change Entities at run time. Coffee1 with Coffee2. What am I missing? – Bryan K Nov 21 '21 at 20:05
  • You posted two snippets of code where the only thing changed was the table you are querying on. You wanted something where you did not have to specifically code each entity right? The answer I posted does just that. It goes through every table and does what your original snippets do but without having to specify each table manually. Or I have misunderstood the question based off the code you posted? – Hank Nov 21 '21 at 23:33
  • Hank, That is correct. I guess the problem is I don't understand how to implement the code. Sorry but this is new to me so I'm not grasping what to do. – Bryan K Nov 21 '21 at 23:52
  • I gave everything that's needed to query all tables. The code I put in my answer does what the two snippets of code you gave but all the entities in all the tables (i.e. makes a `GenericCoffeeList` from each entity and adds it to an Observable collection). Instead of using a single DbSet, the code I gave returns you ALL DbSets (but as `IQueryable` since you don't have a common base class). Ask another question if you need further help with the code you have. – Hank Nov 22 '21 at 14:59
  • Hank, How would I use the iQueryable to get the entity I need? – Bryan K Nov 22 '21 at 17:46
  • You want a specific entity? Just use the properties/entities on your dbcontext. From the looks of it now, you are not wanting a general query on all sets. You can use reflection to get a specific entity but then you are just back to the original problem of hardcoding all the entities you want to use. You asked a similar question a few months back, I'll point you to the comment that was posted b/c it was closed as duplicate: https://stackoverflow.com/questions/52637924/dynamic-dbset-in-entity-framework-core – Hank Nov 22 '21 at 18:06
  • Hank, Thanks I will dig deeper into it tonight when I get home. Thanks for all the help. – Bryan K Nov 22 '21 at 18:14
0

I think, you create genetic class for the query. When you change Object(TEntity), query executed for the Object(TEntity).

public class QueryRepo<TEntity> where TEntity : class
{
    private readonly ctxCoin;
    public QueryRepo(){
        ctxCoin = new CtxCoin();
    }

    public IEnumerable<GenericCoffeeList> GetCoffeeList()
    {
        var entity = ctxCoin.Set<TEntity>();
        return (from c in entity
            select new GenericCoffeeList
            {
                CoffeeCatalogId = c.Id,
                Name = c.Name,
                Type = c.Type
            }).ToList();
    }
}
karagoz
  • 135
  • 5
0

I personally would try using a generic method with a labda which is used to convert the different Coffee types into GenericCoffeeList.

public IEnumerable<GenericCoffeeList> GetCoffeeList<TCoffee>(Func<TCoffee, GenericCoffeeList> toGenericCoffeeList)
{
    return ctxCoin.Set<TCoffee>()
        .Select(coffee => toGenericCoffeeList(coffee)
        .ToList();
}

Doing it this way reduces the amount of code needed and the only thing that needs to be duplciated is the function that is passed as toGenericCoffeeList but also does not require refactoring 200+ classes to implement an interface.

This approach can be adapted depending on what you need to do (I'm not exactly sure what your method is supposed to do because coffeeList is never used and coinList is never declared)

Nannanas
  • 591
  • 2
  • 8
  • Nannana, can TCoffee be a string that is passed in? – Bryan K Dec 15 '21 at 18:23
  • No it needs to be the type you want to query - it would be used as `GetCoffeeList(coffee1 => new GenericCoffeeList { CoffeeCatalogId = coffee1 .Id, Name = coffee1 .Name, Type = coffee1 .Type })` – Nannanas Dec 16 '21 at 07:40
  • Thats what I thought. Thats what I'm trying to avoid. I have 200+ TEntites so I'm looking for a solution where I do not have to hard code each one. – Bryan K Dec 16 '21 at 14:29
0

You can map one Type to another by mapping the fields like this:

public static Expression<Func<T, R>> MapFields<T, R>(IDictionary<string, string> fieldNamesMapping) 
            where R: new()
{
    var parameter = Expression.Parameter(typeof(T), "o");  
             
    return Expression.Lambda<Func<T, R>>(
        Expression.MemberInit(
            Expression.New(typeof(R)), 
            GetMemberBindings<T,R>(fieldNamesMapping, parameter)), 
        parameter);
}
      

private static IEnumerable<MemberBinding> GetMemberBindings<T,R>(IDictionary<string, string> fieldNamesMapping,
    ParameterExpression parameter) 
        => fieldNamesMapping
            .Select(o => (MemberBinding)Expression.Bind(
                typeof(R).GetProperty(o.Value) ?? throw new InvalidOperationException(), 
                Expression.Property(parameter, typeof(T).GetProperty(o.Key) ?? throw new InvalidOperationException())));

This code assumes that the types have properties with similar types but different names. Thus the Dictionary with the corresponding filednames.

If the fields happen to have the same names you could of course infer the properties using reflection.

The usage is like so:

var toBeMapped = new List<FooA> {
                new FooA {A = 1, B = 2, C = 3},
                new FooA {A = 4, B = 5, C = 6},
                new FooA {A = 7, B = 8, C = 9},
                new FooA {A = 10, B = 11, C = 12}
            };

            var result = toBeMapped.AsQueryable().Select(
                MemberBindingExpressions.MapFields<FooA, FooB>(
                    new Dictionary<string, string> {["A"] = "A", ["B"] = "E"})).ToList();

            result[0].Should().Be(new FooB {A = 1, E = 2});
            result[3].Should().Be(new FooB { A = 10, E = 11 });
Clemens
  • 588
  • 6
  • 9