With the first release of EF Core, there wasn't any proper approach of fluent mapping of entities, in an easy manner for enterprises application where a lot of entities needs to be involved. You might have ended up with nasty or repeated codes.
At that time only way was to compose mapping based on ModelBuilder and by using inline fluent mapping codes. Like in newer version EF Core 2.0 there had been a new release of IEntityTypeConfiguration for the creation of mapping based on single entity and could be registered with ApplyConfiguration by passing a new instance of IEntityTypeConfiguration object. https://docs.microsoft.com/en-us/ef/core/what-is-new (Self-contained type configuration for code first)
This is really good for big application as we can separate mappings based on each entity.
In this article, we would see how to achieve similar functionality in EF Core 1.0 and this implementation is a bit cleaner than 2.0 built-in classes. So, in both of versions, this could be used with minimum effort.
The other optional element I have included is to have common Primary Key Name with Identity field based on interface IPrimaryKey<int>. This allows me to have the same name for all Primary Keys and common mapping codes so that I do not have to repeat all over again.
NOTE: This is optional you can remove InitPrimaryKey function if you do not need.
The Map abstract function is important since this is going to be used in each Fluent Mapping classes.
In this, it is inherited from base class EntityTypeBuilderBase<LookupCountry> having constructor accepting table name and schema.
The overridden Map function is available to put fluent mapping. Since I have used my custom interface for Id mapping and it is being mapped on the base class, I have not put fluent mapping for same. Like I mentioned earlier this could be skipped, just that you have to put fluent mapping for primary key manually in that situation.
The key highlights here are Map and BuildMap functions.
The BuildMap function is to create fluent mapping for entities with constraints like it has to be parameterless constructor class and inherited from EntityTypeBuilderBase class. This would allow to us to create a new object of mapper through Generic approach and call methods Init and Map from a base class in a convenient way for mapping.
The internal does not matter since we just need to add a mapping of an entity with related Mapper class in Map function.
It just says entity and Mapping class with ModelBuilder. Simple enough!
Please add these two files into Code Snippet Manager from VS 20XX.
FluentMapping.snippet
FluentRegistration.snippet
At that time only way was to compose mapping based on ModelBuilder and by using inline fluent mapping codes. Like in newer version EF Core 2.0 there had been a new release of IEntityTypeConfiguration for the creation of mapping based on single entity and could be registered with ApplyConfiguration by passing a new instance of IEntityTypeConfiguration object. https://docs.microsoft.com/en-us/ef/core/what-is-new (Self-contained type configuration for code first)
This is really good for big application as we can separate mappings based on each entity.
In this article, we would see how to achieve similar functionality in EF Core 1.0 and this implementation is a bit cleaner than 2.0 built-in classes. So, in both of versions, this could be used with minimum effort.
Steps/approach taken to achieve same.
- A base class which would have certain constraints and common functionality to take care of certain mappings.
- Sample mapping class based on base class for actual fluent mapping implementations.
- Application level fluent mapping with a helper method to create mappings generically by passing entity and associated mapping class.
- Registration of fluent mappings in application DbContext class by just creating an object of global fluent mapping.
Base class implementation for each Fluent mapping classes
I like to have schema name for each table so I have included parameterized constructor for accepting model name and schema name.The other optional element I have included is to have common Primary Key Name with Identity field based on interface IPrimaryKey<int>. This allows me to have the same name for all Primary Keys and common mapping codes so that I do not have to repeat all over again.
NOTE: This is optional you can remove InitPrimaryKey function if you do not need.
/// <summary>
/// Entity builder base class for fluent mapping.
/// </summary>
/// <typeparam name="TModel">The type of the model.</typeparam>
internal abstract class EntityTypeBuilderBase<TModel>
where TModel : class
{
/// <summary>
/// The model name
/// </summary>
private readonly string ModelName;
/// <summary>
/// The model schema
/// </summary>
private readonly string ModelSchema;
/// <summary>
/// Initializes a new instance of the <see cref="EntityTypeBuilderBase{TModel}"/> class.
/// </summary>
/// <param name="modelName">Name of the model.</param>
/// <param name="modelSchema">The model schema.</param>
/// <exception cref="ArgumentNullException">
/// modelName
/// or
/// modelName
/// </exception>
protected EntityTypeBuilderBase(string modelName, string modelSchema)
{
if (IsNullOrEmpty(modelName))
{
throw new ArgumentNullException(nameof(modelName));
}
if (IsNullOrEmpty(modelSchema))
{
throw new ArgumentNullException(nameof(modelName));
}
ModelName = modelName;
ModelSchema = modelSchema;
}
/// <summary>
/// Initializes the specified builder.
/// </summary>
/// <param name="builder">The builder.</param>
public void Init(EntityTypeBuilder<TModel> builder)
{
InitPrimaryKey(builder);
builder.ToTable(ModelName, ModelSchema);
}
/// <summary>
/// Fluent mapping for the model.
/// </summary>
/// <param name="builder">The entity DB builder.</param>
public abstract void Map(EntityTypeBuilder<TModel> builder);
/// <summary>
/// Initializes the primary key.
/// </summary>
/// <param name="entityTypeBuilder">The entity type builder.</param>
private static void InitPrimaryKey(EntityTypeBuilder<TModel> entityTypeBuilder)
{
var primaryKeyBuilder = entityTypeBuilder as EntityTypeBuilder<IPrimaryKey<int>>;
if (primaryKeyBuilder != null)
{
primaryKeyBuilder.HasKey(m => m.Id);
primaryKeyBuilder.Property(m => m.Id).ValueGeneratedOnAdd();
}
}
}
The Map abstract function is important since this is going to be used in each Fluent Mapping classes.
Sample entity mapping class
Suppose we have an entity called LookupCountry. The fluent mapping based on sample model. /// <summary>
/// Fluent mapping for <see cref="LookupCountry"/> entity.
/// </summary>
/// <seealso cref="EntityTypeBuilderBase{Country}" />
internal class LookupCountryMapping
: EntityTypeBuilderBase<LookupCountry>
{
/// <summary>
/// Initializes a new instance of the <see cref="LookupCountryMapping"/> class.
/// </summary>
public LookupCountryMapping()
: base("Country", "Lookup")
{
}
/// <summary>
/// Fluent mapping for the model.
/// </summary>
/// <param name="builder">The entity DB builder.</param>
public override void Map(EntityTypeBuilder<LookupCountry> builder)
{
builder.Property(model => model.Code).HasMaxLength(2);
builder.Property(model => model.IsDisabled);
builder.Property(model => model.Name).HasMaxLength(255);
}
}
In this, it is inherited from base class EntityTypeBuilderBase<LookupCountry> having constructor accepting table name and schema.
The overridden Map function is available to put fluent mapping. Since I have used my custom interface for Id mapping and it is being mapped on the base class, I have not put fluent mapping for same. Like I mentioned earlier this could be skipped, just that you have to put fluent mapping for primary key manually in that situation.
Application level FluentMapping class implementation
This class is the beholder of all fluent mappings for the entire application also it allows registering mappings in an easier manner. Let's first see the code then will have a look on explanation. /// <summary>
/// Fluent Mapping for the application.
/// </summary>
internal class FluentMapping
{
/// <summary>
/// The model builder
/// </summary>
private readonly ModelBuilder ModelBuilder;
/// <summary>
/// Initializes a new instance of the <see cref="FluentMapping"/> class.
/// </summary>
/// <param name="builder">The database model builder.</param>
internal FluentMapping(ModelBuilder builder)
{
ModelBuilder = builder;
}
/// <summary>
/// Fluent mapping for entire application.
/// </summary>
internal void Map()
{
BuildMap<LookupCountry, LookupCountryMapping>(ModelBuilder);
}
/// <summary>
/// Builds the DB model mapping.
/// </summary>
/// <typeparam name="TModel">The type of the model.</typeparam>
/// <typeparam name="TMapper">The type of the mapper.</typeparam>
/// <param name="dbModelBuilder">The database model builder.</param>
private static void BuildMap<TModel, TMapper>(ModelBuilder dbModelBuilder)
where TModel : class
where TMapper : EntityTypeBuilderBase<TModel>, new()
{
var map = new TMapper();
var builder = dbModelBuilder.Entity<TModel>();
map.Init(builder);
map.Map(builder);
}
}
The key highlights here are Map and BuildMap functions.
The BuildMap function is to create fluent mapping for entities with constraints like it has to be parameterless constructor class and inherited from EntityTypeBuilderBase class. This would allow to us to create a new object of mapper through Generic approach and call methods Init and Map from a base class in a convenient way for mapping.
The internal does not matter since we just need to add a mapping of an entity with related Mapper class in Map function.
BuildMap<LookupCountry, LookupCountryMapping>(ModelBuilder);
It just says entity and Mapping class with ModelBuilder. Simple enough!
Registration of entire application fluent mapping on DbContext
This is a really straight forward implementation which does not need any explanation. Just need to create an object of FluentMapping class under OnModelCreating function. public sealed class MyProjectContext
: DbContext
{
public MyProjectContext(DbContextOptions option)
: base(option)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
var mapping = new FluentMapping.FluentMapping(builder);
mapping.Map();
}
}
Bonus material: Code Snippet for creating fluent mapping and registration code for same
If you have come so far, I have some bonus materials for composing FluentMapping class and registration of same (BuildMap code) through Visual Studio 20XX Snippet feature.Please add these two files into Code Snippet Manager from VS 20XX.
FluentMapping.snippet
<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Title>Add A Fluent Mapping</Title>
<Author>Vikash Kumar</Author>
<Description>Fluent mapping structure.</Description>
<Shortcut>FluentMapping</Shortcut>
</Header>
<Snippet>
<Imports>
<Import>
<Namespace>MyProject.DB.FluentMapping.Base</Namespace>
</Import>
<Import>
<Namespace>Microsoft.EntityFrameworkCore.Metadata.Builders</Namespace>
</Import>
</Imports>
<Declarations>
<Literal>
<ID>modelname</ID>
<ToolTip>Replace with data model name</ToolTip>
<Default>MyModel</Default>
</Literal>
</Declarations>
<Code Language="CSharp">
<![CDATA[
/// <summary>
/// Fluent mapping for <see cref="$modelname$"/> entity.
/// </summary>
/// <seealso cref="EntityTypeBuilderBase{$modelname$}" />
internal class $modelname$Mapping
: EntityTypeBuilderBase<$modelname$>
{
/// <summary>
/// Initializes a new instance of the <see cref="$modelname$Mapping"/> class.
/// </summary>
public $modelname$Mapping()
: base(nameof($modelname$), Schema.Name)
{
}
/// <summary>
/// Fluent mapping for the model.
/// </summary>
/// <param name="builder">The entity DB builder.</param>
public override void Map(EntityTypeBuilder<$modelname$> builder)
{
}
}
]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>
FluentRegistration.snippet
<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Title>Fluent Mapping registration</Title>
<Author>Vikash Kumar</Author>
<Description>Fluent mapping registration.</Description>
<Shortcut>FluentMappingRegistration</Shortcut>
</Header>
<Snippet>
<Declarations>
<Literal>
<ID>model</ID>
<ToolTip>Model to map</ToolTip>
<Default>MyModel</Default>
</Literal>
<Literal>
<ID>param</ID>
<ToolTip>Model Builder Parameter.</ToolTip>
<Default>ModelBuilder</Default>
</Literal>
</Declarations>
<Code Language="csharp"><![CDATA[BuildMap<$model$, $model$Mapping>($param$);$end$]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>
Comments
Post a Comment