1

I'm trying to write a generic IEntityTypeConfiguration for an entity that has a generic type parameter.

I have the following class:

public class ItemSpecs<TItemConstitution> : Entity, IItemSpecs<TItemConstitution>
   where TItemConstitution : Enum
  {
    public Guid ItemSpecsId { get; set; }
    public decimal OtherProperty { get; set; }
    public virtual TItemConstitution Property { get; set; }
  }

  public enum ParentItemProperty
  {
    OnlyParent,
    MultiParent
  }

  public enum ChildItemProperty
  {
    OnlyChild,
    MultipleChild
  }

  public interface IItemSpecs<TItemConstitution>
  {
    public TItemConstitution Property { get; set; }
  }

Some items will be ItemSpecs<ParentItemProperty> others will be ItemSpecs<ChildItemProperty>. Since both instances have an Enum Property and Enums get stored as ints in the Db, my idea was to store them in the same DbSet.

When I want to configure ItemSpecs<TItemConstitution> through IEntityTypeConfiguration:

public class ItemSpecsTypeConfig : IEntityTypeConfiguration<ItemSpecs<Enum>>
  {
    public void Configure(EntityTypeBuilder<ItemSpecs<Enum>> builder)
    {
      builder.HasKey(x => x.ItemSpecsId);
    }
  }

Things seem to be okay, but when I want to Add an item to

public DbSet<ItemSpecs<Enum>> ItemSpecs{ get; set; }

I get the following

InvalidOperationException : The entity type 'ItemSpecs<ParentItemProperty>' requires a primary key to be defined. If you intended to use a keyless entity type, call 'HasNoKey' in 'OnModelCreating'.

I'm a little confused by that exception, since the ItemSpecs-class is referenced to in the ItemSpecsTypeConfig-class and has a Key defined. I don't intend to use keyless entitytype.

Can anyone explain this error, as to where it's coming from and how to work with it? Official Documentation makes no mention of this use case at all.

Do I need an IEntityTypeConfiguration<IItemSpecs<>> for each enum and write to same table or can I make a generic IEntityTypeConfiguration for all implementation? I'd like to avoid having DbSets for every type of ItemSpecs. Thanks for any information or example

Update:

The Exception I was getting

InvalidOperationException : The entity type 'ItemSpecs<ParentItemProperty>' requires a primary key to be defined. If you intended to use a keyless entity type, call 'HasNoKey' in 'OnModelCreating'.

Was caused by (Thank you @Eldar) not having an implementation for IEntityTypeConfiguration<ItemSpecs<ParentItemProperty> and IEntityTypeConfiguration<ItemSpecs<ChildItemProperty>

After I added those, the exception changed to

InvalidOperationException : The entity type 'ItemSpecs<Enum>' requires a primary key to be defined. If you intended to use a keyless entity type, call 'HasNoKey' in 'OnModelCreating'

which comes from my DbContext public

public DbSet<ItemSpecs<Enum>> ItemSpecs{ get; set; }

It makes sense to me that DbSet<ItemSpecs<Enum>> is not a correct definition, but refactoring to having a DbSet<ItemSpecs<TEnum>> would require me to know this Generic Type Argument everytime I inject the DbContext (right?)

So this is my question:

How do I define a DbSet for an entity with a generic type parameter? The goal is to have 1 DbSet for all possible generic implementations. Querying and persisting happens (mostly) through reflection, so not really an issue for this question. Also, the ItemSpecs class is always a nested class.

BHANG
  • 354
  • 1
  • 5
  • 16
  • 1
    Not tested it yet but I can say, you can have a generic DbSet but you need to apply configuration for all the possible types. (You can use reflection here) – Eldar Jun 22 '23 at 19:10
  • @Eldar Yes, thanks, I'm testing it out now with IEntityTypeConfiguration> per EnumType, seems to resolve the issue I had. But throws another Exception, instead of The entity type 'ItemSpecs' requires a primary key to be defined. Now its for 'ItemSpecs'. I'm investigating why and how to fix, (could be VS caching too) I'll report on my findings later! – BHANG Jun 22 '23 at 19:19
  • 1
    Btw is this your generic dbset: `public DbSet> ItemSpecs{ get; set; }`? You need to have a generic method that returns a dbset as the properties can not be generic. – Eldar Jun 22 '23 at 19:26
  • @Eldar That is indeed the generic dbset I'm using atm. Good Tip, thank you, had not thought about that, so will look into it a bit, but makes sense there can be no DbSet for the Generic Type. Should you find any examples of that type of generic method on S/O, don't hesitate to share here too, since I'm very new to an approach like that! Either way, thank you already! – BHANG Jun 22 '23 at 19:40
  • 1
    Something like this : `public DbSet> GetItemSpecs() where TEnum : Enum` – Eldar Jun 22 '23 at 19:51
  • @Eldar Does that method go into the DbContext? Since all my DbSets are properties, I don't see how a method would be an equivalent to a property? I feel like you're on the right track, but maybe I'm not understanding you completely. – BHANG Jun 23 '23 at 10:52
  • Yes, it goes into the DbContext if you want something generic. – Eldar Jun 23 '23 at 11:43
  • @Eldar Do you mean I should declare the DbSet> in DbContext, make that it can be overwritten when querying entities through an injected dbContext? And to overwrite the generic ItemSpecs I would need a generic method as you suggested that overwrites with the desired enum for generic type parameter? A bit like https://stackoverflow.com/questions/57551291/how-to-pass-an-string-as-a-type-parameter-for-a-generic-dbset-in-entity-framewor – BHANG Jun 23 '23 at 12:06
  • Yes it can be used like this: `var parentItemProperties = await dbContext.GetItemSpecs().ToListAsync()` – Eldar Jun 23 '23 at 15:10
  • I think I should have mentioned, these ItemSpecs are in fact nested classes. I will never have to get or set them directly. So, I think I don't have a real issue with querying or writing the data (that happens mostly through reflection), but the definition of my DbSets is where I struggle: public property DbSet> ItemSpecs{ get; set; } on my DbContext throws an error as mentioned, but to replace 'Enum' with 'TEnum', I would need to pass my generic type param along with the DbContext, every time I need to inject it, and that seems very wrong ;-) Thanks 4 your patience btw! – BHANG Jun 23 '23 at 16:28

1 Answers1

1

The solution is to work with an abstract base class (and separate IEntityTypeConfiguration) to configure the keys:

public abstract class ItemSpecsBase
{    
  public Guid ItemSpecsId { get; set; }    
  public decimal OtherProperty { get; set; }
}

public class ItemSpecsBaseTypeConfig : IEntityTypeConfiguration<ItemSpecsBase>  
{
  public void Configure(EntityTypeBuilder<ItemSpecsBase> builder)
  {
    builder.HasKey(x => x.ItemSpecsBaseId);     
  }
}

To also add IEntityTypeConfigurations per TEnum, so we can write to the same database table:

public class ParentItemSpecsBaseTypeConfig : IEntityTypeConfiguration<ParentItemProperty>  
{
  public void Configure(EntityTypeBuilder<ParentItemProperty> builder)
  {
    builder.ToTable("ItemSepcs");     
  }
}

public class ChildItemSpecsBaseTypeConfig : IEntityTypeConfiguration<ChildItemProperty>  
{
  public void Configure(EntityTypeBuilder<ChildItemProperty> builder)
  {
    builder.ToTable("ItemSepcs");     
  }
}

Then make the class inherit from the base class

public class ItemSpecs<TItemConstitution> : ItemSpecsBase, IItemSpecs<TItemConstitution>
  where TItemConstitution : Enum
{   
  public virtual TItemConstitution Property { get; set; }
}

In DbContext, we make DbSets per TEnum. Since we write them to the same database table, this does not really impact the DB

public DbSet<ItemSpecs<ParentItemProperty>> ParentItemSpecs { get; set; }
public DbSet<ItemSpecs<ChildItemProperty>> ChildItemSpecs { get; set; }

Interface can stay the same, but is not really relevant to this question

public interface IItemSpecs<TItemConstitution>
  where TItemConstitution : Enum
{
  TItemConstitution Property { get; set; }
  Guid ItemSpecsId { get; set; }
  decimal OtherProperty { get; set; }
}
BHANG
  • 354
  • 1
  • 5
  • 16