Home > Blockchain >  Dynamically calling collections with different types from a Dictionary
Dynamically calling collections with different types from a Dictionary

Time:11-07

I will try to summarize what I'm trying to do first in text. I have a dictionary of DbSets that has a value of other collections (in my case either a IEnumerable or List). Through a foreach loop, Im trying to access the DbSet first to see if it contains 'Any' items. If this is not the case, then I want to continue to the value, so that I can add defined values within a IEnumerable/List into the DbSet. At last, I save changes with EntityFrameworkCore. However, some I cannot access these DbSets or Collections, since during run-time they needed to be solved. Maybe you have a better way of doing this, because the entities differ from each other that are present as keys and values within the dictionary... So what I was actually expecting was that it would be possible to gain access to these DbSets (keys)/Collections (values) in a dynamic way, where I do not have to declare types on beforehand. A dictionary does only allow a single type, so I decided to go for a 'dynamic' type. I didn't know what other options are there to do this a little bit clean...

Let me summarize first which types I have for the DbSets (which are keys in my dictionary):

  • (KEY 1) 'configContext!.Clients' has actual type: Duende.IdentityServer.EntityFramework.Entities.Client
  • (KEY 2) 'configContext!.IdentityResources' has actual type: Duende.IdentityServer.EntityFramework.Entities.IdentityResource
  • (KEY 3) 'configContext!.ApiScopes' has actual type: Duende.IdentityServer.EntityFramework.Entities.ApiScope
  • (KEY 4) 'configContext!.ApiResources' has actual type: Duende.IdentityServer.EntityFramework.Entities.ApiResource

For the values, other types are being used

  • (VALUE OF KEY 1) 'Config.Clients' has actual type: Duende.IdentityServer.Models.Client
  • (VALUE OF KEY 2) 'Config.IdentityResources' has actual type: Duende.IdentityServer.Models.IdentityResource
  • (VALUE OF KEY 3) 'Config.ApiScopes' has actual type: Duende.IdentityServer.Models.ApiScope
  • (VALUE OF KEY 4) 'Config.ApiResources' has actual type: Duende.IdentityServer.Models.ApiResource

NOTE

Although I have different types each key can be mapped from model to entity or from entity to model. So keys and values have 1:1 relationship. Difference between keys and values is that Keys are Database Entities and Values are Models

My initial code:

Using part:

    using Duende.IdentityServer.EntityFramework.DbContexts;
    using Duende.IdentityServer.EntityFramework.Mappers;
    using Duende.IdentityServer.Models;
    using Microsoft.EntityFrameworkCore;
    using DbClient = Duende.IdentityServer.EntityFramework.Entities.Client;
    using DbIdentityResource = Duende.IdentityServer.EntityFramework.Entities.IdentityResource;
    using DbApiScope = Duende.IdentityServer.EntityFramework.Entities.ApiScope;
    using DbApiResource = Duende.IdentityServer.EntityFramework.Entitie  s.ApiResource;

Test method:

    private static void Test(this ConfigurationDbContext configContext)
        {
            Dictionary<dynamic, dynamic> configCollections = new Dictionary<dynamic, dynamic>()
            {
                { configContext!.Clients, Config.Clients },
                { configContext!.IdentityResources, Config.IdentityResources },
                { configContext!.ApiScopes, Config.ApiScopes },
                { configContext!.ApiResources, Config.ApiResources },
            };

            foreach (var configCollection in configCollections)
            {
                var collection = configCollection!.Key.Any();

                if (collection == false)
                {
                    foreach (var configValue in configCollection.Value)
                    {
                        configCollection!.Key.Add(configValue.ToEntity());
                    }

                    configContext.SaveChanges();
                }
            }
        }

How is the code being called (initial version)

    configContext.Test();

Initial error I received, before making any new attempts:

Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: ''Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable<Duende.IdentityServer.EntityFramework.Entities.Client>' does not contain a definition for 'Any''

What I have tried to fix it, but without success

MY NEW 1ST ATTEMPT: (first changed dictionary and then method call)

Test Method looked like this:

    private static void Test<T1, T2, T3, T4, K1, K2, K3, K4>(this ConfigurationDbContext configContext)

My new dictionary:

    Dictionary<dynamic, dynamic> configCollections = new Dictionary<dynamic, dynamic>()
            {
                { configContext!.Clients.Cast<T1>(), Config.Clients.Cast<K1>() },
                { configContext!.IdentityResources.Cast<T2>(), Config.IdentityResources.Cast<K2>()  },
                { configContext!.ApiScopes.Cast<T3>(), Config.ApiScopes.Cast<K3>() },
                { configContext!.ApiResources.Cast<T4>(), Config.ApiResources.Cast<K4>() },
            };

Method call:

    configContext.Test<DbClient, DbIdentityResource, DbApiScope, DbApiResource, Client, IdentityResource, ApiScope, ApiResource>();

MY NEW 2ND ATTEMPT: changed var collection to ->

    ((DbSet<dynamic>)configCollection!.Key).Any();

MY NEW 3RD ATTEMPT: changed var collection to ->

    ((DbSet<dynamic>) configCollection!.Key).Cast<dynamic>().Any();

New errors I have achieved...

Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 'Cannot convert type 'Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable<Duende.IdentityServer.EntityFramework.Entities.Client>' to 'Microsoft.EntityFrameworkCore.DbSet''

Does anyone have a clue about what is going on? I'd appreciate it.

UPDATE: SOLVED PROBLEM THROUGH CODE BELOW:

Note:

  1. using Duende.IdentityServer.EntityFramework.Mappers has different mappers per config type (just mentioning, because they do not exist under Mappers and exist in a deeper layer in Mappers)
  2. Make sure to add the following to your connection string "MultipleActiveResultSets=true;"
    public static void TestExec()
        {
            List<(IQueryable dbSet, IEnumerable<object> models)> configCollections = new() 
            {
                (configContext!.Clients, Config.Clients),
                (configContext!.IdentityResources, Config.IdentityResources),
                (configContext!.ApiScopes, Config.ApiScopes),
                (configContext!.ApiResources, Config.ApiResources),
            };

            foreach (var (dbSet, models) in configCollections)
            {
                if (dbSet.AsQueryable().GetEnumerator().MoveNext() == false)
                { 
                    foreach (object configValue in models)
                    {
                        dynamic? test = null;

                        switch (configValue.GetType().Name)
                        {
                            case nameof(Client):
                                test = ClientMappers.ToEntity((dynamic)configValue);
                                break;
                            case nameof(IdentityResource):
                            case nameof(OpenId):
                            case nameof(Profile):
                            case nameof(Email):
                            case nameof(Phone):
                            case nameof(Address):
                                test = IdentityResourceMappers.ToEntity((dynamic)configValue);
                                break;
                            case nameof(ApiScope):
                                test = ScopeMappers.ToEntity((dynamic)configValue);
                                break;
                            case nameof(ApiResource):
                                test = ApiResourceMappers.ToEntity((dynamic)configValue);
                                break;
                            default:
                                break;
                        }

                        ((dynamic)dbSet).Add(test!);
                        
                    }

                    configContext.SaveChanges();
                }
            }
        }

CodePudding user response:

I am not sure whether a dictionary is the best choice here. The advantage of a dictionary over a list is that you can lookup a value very quickly (O(1)) by using its key. If you are simply enumerating keys or values or key/value-pairs, it is slower than a list (both O(n)).

Do not use a dictionary if you simply want to store value pairs. You can do so by storing tuples in a list for example. In the examples you have given, you are never looking up a value by key. This is what confused me in your question and comments. You never actually use Key as a key but only as a storage.

This shows how you can store the information in a list of tuples:

List<(IQueryable dbSet, IEnumerable models)> configCollections = new() {
    (configContext!.Clients, Config.Clients),
    (configContext!.IdentityResources, Config.IdentityResources),
    (configContext!.ApiScopes, Config.ApiScopes),
    (configContext!.ApiResources, Config.ApiResources),
};

You can then write your test as

foreach (var (dbSet, models) in configCollections) {
    if (!dbSet.AsQueryable().GetEnumerator().MoveNext()) { // !Any()
        foreach (object configValue in models) {
            ((dynamic)dbSet).Add(((dynamic)configValue).ToEntity());
        }

        configContext.SaveChanges();
    }
}

If your config value classes have a common non-generic base class or interface, then you can use a IEnumerable<CommonBase> instead of IEnumerable. If the common base contains a non-generic ToEntity() method, then you can simplify adding values to the dbset

            ((dynamic)dbSet).Add(configValue.ToEntity());

You can still use a dictionary, but then the key should be the type of the entities you want to retrieve. The dictionary would be declared as

Dictionary<Type, (IQueryable dbSet, IEnumerable models)> configCollections;

and you could retrieve the collections with

if (configCollections.TryGetValue(typeof(Client), out var collections)) {
    var (dbSet, models) = collections;
    // TODO: Do something with dbSet or models
}
  • Related