0

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();
                }
            }
        }

user20291437
  • 99
  • 1
  • 8
  • It is not clear to me what the keys are. Are they an object of some type or should they be the very type of this object? – Olivier Jacot-Descombes Nov 02 '22 at 15:21
  • @OlivierJacot-Descombes They are 'DbSets' and in the very beginning of my question I summarized which types I have, so that you can see that keys and values differ from each other. Mainly, I just try to access the DbSets and those values (which are other collections) during run-time.. but since I used 'dynamic' as type and I have multiple similar types (1:1 relationship with key and value), somehow during run time keys cannot be mapped... If you have a better approach, please let me know. Difference between keys and values is that Keys are Database Entities and Values are Models. – user20291437 Nov 02 '22 at 17:46
  • I suggest using `Dictionary`. Then you can get the key with e.g. `typeof(Duende.IdentityServer.EntityFramework.Entities.Client)` or from an object with `obj.GetType()`. – Olivier Jacot-Descombes Nov 03 '22 at 10:22
  • The thing is, I want to a have listX inside the dictionary as a key that has a value of another list. I do not know the types, so the types should be resolved at run time.. so I cannot use 'typeof(Duende.IdentityServer.EntityFramework.Entities.Client)' – user20291437 Nov 03 '22 at 22:03
  • But you can use `list.GetType()` if you interested in the type. But I still do not understand what the key should be: 1. the type of the list, 2. the type of the list items, 3. the list reference, 4. all the values in the list. – Olivier Jacot-Descombes Nov 04 '22 at 13:46
  • @OlivierJacot-Descombes the key is a list reference from configContext but it has all the values from that list. I'm not interested in the type, I just want to be able to call for example 'Any()' on that dynamic key. But I get an error back which I mentioned earlier under 'Initial error I received, before making any new attempts'. – user20291437 Nov 05 '22 at 21:45

1 Answers1

1

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
}
Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
  • If I run this example, ToEntity(/ToModel) from package: using Duende.IdentityServer.EntityFramework.Mappers; is not recognized, whereafter I receive the following message after running: Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: ''Duende.IdentityServer.Models.Client' does not contain a definition for 'ToEntity'' – user20291437 Nov 06 '22 at 16:54
  • Sorry, but I don't know this `ToEntity` method. If it is an extension method, try calling `Duende.IdentityServer.EntityFramework.Mappers.ToEntity(configValue)` instead. If it is generic, then you will have a problem. – Olivier Jacot-Descombes Nov 06 '22 at 17:02
  • Ok, basically Duende.IdentityServer provides some mappers and for each separate type you have different mappers. I have declared something like this now: https://ibb.co/zZP55Pq but if I run this part, the crash happens at "((dynamic)dbSet).Add(test!);" and says the following: Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 'The best overloaded method match for 'Microsoft.EntityFrameworkCore.DbSet.Add(Duende.IdentityServer.EntityFramework.Entities.Client)' has some invalid arguments' – user20291437 Nov 06 '22 at 17:16
  • If I change the part "object? test = null;" to "dynamic? test = null;" then somehow it does not crash. This time it crashes at 'configContext.SaveChanges();' and I get the following error... : System.InvalidOperationException: 'There is already an open DataReader associated with this Connection which must be closed first.' – user20291437 Nov 06 '22 at 17:18
  • See: [Entity Framework: There is already an open DataReader associated with this Command](https://stackoverflow.com/questions/4867602/entity-framework-there-is-already-an-open-datareader-associated-with-this-comma). Please post new questions if you encounter new problems. – Olivier Jacot-Descombes Nov 06 '22 at 17:35
  • I was literally about to quote the following link: https://stackoverflow.com/questions/4867602/entity-framework-there-is-already-an-open-datareader-associated-with-this-comma/34185390#34185390 – user20291437 Nov 06 '22 at 17:36
  • That is how I solved it now. Your suggestion with that switch case and adding "MultipleActiveResultSets=true" to my connection string, I was able to solve this problem! – user20291437 Nov 06 '22 at 17:37
  • I'll put an overview in my question of how I have solved it now with the help of yours, by sharing the complete code. I appreciate your answer so much, thank you Olivier. – user20291437 Nov 06 '22 at 17:38
  • I have a one last question. I was trying to figure out on my own how I could call something like: dbSet.FirstOrDefault(). However, after running this part of the code I end up with an exception which says that it couldn't find this definition for dbSet. Any ideas how to solve this part? – user20291437 Nov 06 '22 at 22:44
  • This happens because the LINQ extension methods are defined on the generic `IQueryable` interface only. But `dbSet` has the neutral non-generic type `IQueryable`. This dynamic approach will always suffer from this problem. Maybe this article can help. It uses Reflection to do the magic: [Calling Generic Methods from Non-Generic Code in .Net](https://jeremydmiller.com/2020/07/27/calling-generic-methods-from-non-generic-code-in-net/). – Olivier Jacot-Descombes Nov 07 '22 at 12:23
  • Thank you Olivier. I could not thank you enough for how you helped me through with this. Kind regards – user20291437 Nov 07 '22 at 16:04