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:
- 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)
- 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
}