Skip to content

Class Mappings

The EFCore Module supports multiple methods of configuring your mapping your classes to database models.

You'll see in many places over the internet that the DbContext class has an OnModelCreating method that allows you to configure the model. We do not recommend it. We recommend using IEntityTypeConfiguration<TDbContext, TEntity> instead.

Note

Try to always use Entity Type Configurations. Certain features, like Generic Repositories only work when using them.

Class Mappings in separate class

The recommended approach for mapping entities to DB models when using EFCore is by implementing IEntityTypeConfiguration<TDbContext, TEntity>. For example:

C#
1
2
3
4
5
6
7
8
9
internal class InspectionEntityTypeConfiguration
    : IEntityTypeConfiguration<MyAppDbContext, Inspection>
    {
        public void Configure(EntityTypeBuilder<Inspection> builder)
        {
            builder.HasKey(x => x.Id);
            builder.Property(x => x.Description);
        }
    }

Note that we are configuring all of our entity fields, but we are also explicitly referencing the DbContext type. This will map our entity with the DbContext you specify as the first generic param.

Note

The IEntityTypeConfiguration<TDbContext, TEntity> is a custom symbol from the Suite. EFCore also includes an IEntityTypeConfiguration<TEntity> that is not tied to a DbContext; these are not considered valid for the EFCore Module and they will be ignored.

Inheritance

If you have a base class, you can map it in a separate IEntityTypeConfiguration<YourBaseClass> and a table per class inheritance will be assumed on the database.

Table-per-type (TPT) Configuration

In the TPT mapping pattern, all the types are mapped to individual tables. Properties that belong solely to a base type or derived type are stored in a table that maps to that type. You need to use the ToTable method for each entity configuration to achieve this. Let's see an example, if these are the model entities:

C#
1
2
3
4
5
6
7
8
9
public class Item : BaseEntity<Guid>
{
    public string Name { get; set; }
}

public class TextItem : Item
{
    public string Value { get; set; }
}

And the configuration for each entity:

C#
internal class TextItemEntityTypeConfiguration
    : IEntityTypeConfiguration<InspectionsDbContext, Item>
{
    public void Configure(EntityTypeBuilder<Item> builder)
    {
        builder.HasKey(x => x.Id);

        builder.Property(x => x.Name);
    }
}

internal class TextItemEntityTypeConfiguration
    : IEntityTypeConfiguration<InspectionsDbContext, TextItem>
{
    public void Configure(EntityTypeBuilder<TextItem> builder)
    {
        // Required for TPT
        builder.ToTable("TextItem");

        builder.Property(x => x.Value);
    }
}

If you don't use the method ToTable in the configuration of the derived entity, the default convention is that the entities are mapped in the Table-per-hierarchy (TPH) pattern. This means that all the properties (from base and derived entities) are stored in the same table. For more information you can check the EF Inheritance docs from Microsoft.

Class Mappings Best Practices

  1. Always use IEntityTypeConfiguration<TDbContext, TEntity> instead of OnModelCreating for mapping your entities.
  2. Place your class mappings inside a ClassMappings folder of your infrastructure layer. Wether that's a folder or a different csproj.
  3. The ClassMappings folder should be next to its DbContext.

Entity Type Decorators

In most scenarios Entity Type Configurations will be just fine.

However, the Suite provides a way for mapping all classes that implements an interface.

This is quite useful for modules that want to extend entities through composition instead of inheritance.

Let's see an example, say we have an interface with some fields and an entity implementing it.

C#
public interface IWithSuperField
{
    string SuperField { get; set; }
}

public class MyEntityWithSuperField : IWithSuperField
{
    public string SuperField { get; set; }

    public int Id { get; set; }
}

We can map the entity like we normally would, note that we don't map IWithSuperField fields.

C#
1
2
3
4
5
6
7
8
9
internal class MyEntityWithSuperFieldntityTypeConfiguration
    : IEntityTypeConfiguration<MyAppDbContext, MyEntityWithSuperField>
{
    public void Configure(EntityTypeBuilder<MyEntityWithSuperField> builder)
    {
        // Only map MyEntityWithSuperField fields.
        builder.HasKey(x => x.Id);
    }
}

And we can define a decorator for all classes that implement IWithSuperField like so:

C#
internal class WithSuperFieldDecorator<TEntity>
    : IEntityTypeDecorator<TEntity>
    where TEntity : class, IWithSuperField
{
    public void Decorate(EntityTypeBuilder<TEntity> builder)
    {
        builder.Property(x => x.SuperField)
            .HasColumnName("SuperFieldName")
            .IsRequired(true);
    }
}

Things to note:

  1. Decorators must be a single generic class.
  2. They must implement IEntityTypeDecorator<TEntity>, where TEntity is the decorator's generic parameter.
  3. The interface that will be decorated is The constraint applied to the decorator's generic parameter.

The signature is important, since decorators acts like a proxy when configuring each of the entities that implement the interface being decorated.

Note

In order to simplify things and prevent confusion, only a single interface can be decorated at a time by a decorator.

Adding entities from Framework Modules

When creating Framework Modules, meaning modules that are intended for other developers to use it may be required to add entities to their model.

In which case, we need to declare the entities without DbContext and "attach" them to the developer's DbContext in runtime.

We can use the EntityFrameworkCoreModuleOptions.AddEntityTypeConfigurationsFromAssembly in order to discover all EntityTypeConfiguration<TEntity> and register them to the provided DbContext Type.

Important

When declaring entities this way, do not create a DbContext in the Framework Module. Declare configurations using EntityTypeConfiguration<TEntity>without the TDbContext variant.

Below is a complete example of an AuditTrailsModule that needs to add entities to its consumer modules.

C#
public class AuditTrailsModuleOptions
{
    internal Type dbContextType { get; private set; }

    public AuditTrailsModuleOptions UseDbContext<TDbContext>()
        where TDbContext : DbContext
    {
        this.dbContextType = typeof(TDbContext);
        return this;
    }
}

public class AuditTrailsModule
    : SuiteModule
{
    public override void SetupModule(IModuleBuilder builder)
    {
        base.SetupModule(builder);

        builder.DependsOn<EntityFrameworkCoreModule>();

        builder.AddOptions<AuditTrailsModuleOptions>();
    }

    public override void ConfigureServices(
        IServiceCollection services,
        ModuleConfigurationContext context)
    {
        base.ConfigureServices(services, context);

        var options = context.GetModuleOptions<AuditTrailsModuleOptions>().Value;
        var efOptions = context.GetModuleOptions<EntityFrameworkCoreModuleOptions>().Value;

        if (options.dbContextType is null)
        {
            throw new InvalidOperationException(
                $"{nameof(AuditTrailsModuleOptions.UseDbContext)} should be called when depending on {nameof(AuditTrailsModule)}");
        }

        // Discover and register all EntityTypeConfiguration<T> from this assembly
        // and register them into the DbContext that was provided.
        efOptions.AddEntityTypeConfigurationsFromAssembly(
            this.GetType().Assembly,
            options.dbContextType);
    }
}

// Notice we are using the variant without DbContext!
public class MyEntityConfiguration : IEntityTypeConfiguration<MyEntity> { ... }

When another module wants to depend on us, they must do it like so:

C#
1
2
3
4
builder.DependsOn<AuditTrailsModule, AuditTrailsModuleOptions>(options =>
{
    options.UseDbContext<UsersCatalogDbContext>();
});