Entity Framework Code First mapping

Fluent API

EF is also an ORM tool, and mapping is always the core part. Therefore, next, the mapping configuration of EF in Code First mode is introduced in detail.

There are two ways to implement the mapping model through Code First: Data Annotation and Fluent API.

Data Annotation needs to represent the mapping information such as primary key and foreign key in the form of Attribute on the Attribute of entity class (I usually call it Plain Object).
This method does not meet the requirements of decoupling, so it is generally not recommended.

The second way is to focus on the Fluent API. The configuration method of Fluent API decouples the entity class from the mapping configuration, which is conducive to the expansion and maintenance of the project.

The core object in Fluent API mode is DbModelBuilder.

In the OnModelCreating method of the rewritten DbContext, we can configure the mapping of an entity as follows:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>().HasKey(t => t.Id);
     
    base.OnModelCreating(modelBuilder);
} 

One problem with using the above method is that the OnModelCreating method will become larger and larger with the increase of mapping configuration.
A better way is to inherit entitytypeconfiguration < EntityType > and add mapping code to this class, such as:

public class ProductMap : EntityTypeConfiguration<Product>
{
    public ProductMap()
    {
        this.ToTable("Product");
        this.HasKey(p => p.Id);
        this.Property(p => p.Name).IsRequired(); 
    }
} 

Then add the instance of this class to the Configurations of modelBuilder.

modelBuilder.Configurations.Add(new ProductMap()); 

If you do not want to manually add custom mapping configuration class objects one by one, you can also use reflection to add all entitytypeconfiguration < > in the assembly to ModelBuilder at one time In the configurations collection
, the following code shows this operation (the code comes from the nopCommerce project):

var typesToRegister = Assembly.GetExecutingAssembly().GetTypes()
.Where(type => !String.IsNullOrEmpty(type.Namespa`Insert code slice here`ce))
.Where(type => type.BaseType != null && type.BaseType.IsGenericType && type.BaseType.GetGenericTypeDefinition() == typeof(EntityTypeConfiguration<>));
foreach (var type in typesToRegister)
{
    dynamic configurationInstance = Activator.CreateInstance(type);
    modelBuilder.Configurations.Add(configurationInstance);
} 

In this way, OnModelCreating is greatly simplified. Once and for all, adding a new entity mapping in the future only needs to add a new XXXMap class inherited from entitytypeconfiguration < > without modifying the OnModelCreating method.

This method provides the best decoupling between entities and mappings, which is highly recommended.

Automatic discovery of EF CodeFirst
For example, there is an entity class named Employee in our program, for which we do not define mapping configuration (entitytypeconfiguration < Employee >), but if we call it with code like the following, EF will automatically create a default mapping for Employee and perform a series of operations such as migration.

var employeeList = context.Set<Employee>().ToList(); 

Of course, in order to provide more flexible configuration mapping, it is recommended to manually create entitytypeconfiguration < employee >.

In the other two cases, EF will also automatically create mappings.

1. The object of class a exists as a navigation attribute of class B. if class B is included in EF mapping, EF will also create a default mapping for class A.

2. Class a inherits from class B. If one of class A or class B is included in EF mapping, EF will also create a default mapping for the other (and use TPH method. See mapping advanced topic below for details).

From the above introduction, we can see that the EntityTypeConfiguration class is the core of the Fluent API. Let's take the method of EntityTypeConfiguration as the line,
Learn how to configure Fluent API in turn.

EntityTypeConfiguration basic method

ToTable: Specifies the name of the database table to map to.

HasKey: configure primary key (also used to configure associated primary key)

Property: this method returns the object of PrimitivePropertyConfiguration. Depending on the property, it may be the object of subclass StringPropertyConfiguration. Through this object, you can configure attribute information in detail, such as IsRequired() or HasMaxLength(400).

Ignore: specifies which attribute to ignore (not mapped to the data table)

For basic mapping, these methods cover almost everything. Here is a comprehensive example:

ToTable("Product");
ToTable("Product","newdbo");//Specify a schema without using the default dbo
HasKey(p => p.Id);//Common primary key
HasKey(p => new {p.Id, p.Name});//Associated primary key
Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);//Do not let the primary key be automatically generated as Identity
Property(p => p.Name).IsRequired().HasMaxLength(20).HasColumnName("ProductName").IsUnicode(false);//Non empty, maximum length 20, custom column name, column type varchar instead of nvarchar
Ignore(p => p.Description); 
  • Using ModelBuilder HasDefaultSchema("newdbo"); You can specify a schema for all mapping entities.
  • PrimitivePropertyConfiguration also has many configurable options, such as HasColumnOrder to specify the order of columns in the table, IsOptional to specify whether columns can be empty, HasPrecision to specify the precision of floating-point numbers, and so on.

EntityTypeConfiguration Association

The protagonist of the following series of examples is the product. In order to cooperate with the demonstration, we also invited product partners, who will appear one by one during the demonstration.

Basically, the configuration of the association shown below can start from the entitytypeconfiguration < T > of any party of the Association class. No matter which side starts to configure different writing methods, the same effect can be achieved in the end.
The following example will show only one of the configurations, and the equivalent configuration will not be shown again.

The basic structure of the product class is as follows. New attributes will be added as needed in the later demonstration process.

public class Product
{
    public int Id{ get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
} 

1 - 1 Association

(although it looks the simplest, it seems to be the most troublesome configuration to understand)

In terms of actual relationship, the two classes share the same value as the primary key. For example, there are User table and UserPhoto table. They should both use UserId as the primary key and associate through the same UserId value.
However, this relationship must be realized through the concept of foreign key in the database. At this time, the primary key of a table is needed as both the primary key and the foreign key of the associated table.
The various configuration methods in EF are nothing more than telling EF CodeFirst to use the primary key of that table as the foreign key of another table. What you don't understand now is to take a look at the following example.
(in fact, if you use Data Annotation to configure, it's very simple. Just mark [key] and [ForeignKey])

This section uses the role of warranty card. We know that a product corresponds to a warranty card, and the product and warranty card use the same product number. This is what we call a good example of 1-to-1.

public class WarrantyCard
{
    public int ProductId { get; set; }
    public DateTime ExpiredDate { get; set; }
    public virtual Product Product { get; set; }
} 

We also add warranty card attributes to Product:

public virtual WarrantyCard WarrantyCard { get; set; } 

Let's take a look at how to associate Product with warranty card. After thousands of attempts, we finally found the correct combination of the following results, which are listed below first,
Slowly analyze later:

public class ProductMap : EntityTypeConfiguration<Product>
{
    public ProductMap()
    {
        ToTable("Product");
        HasKey(p => p.Id);
 
        //Group 1 (two effects are identical)
        HasRequired(p => p.WarrantyCard).WithRequiredDependent(i => i.Product);
        HasRequired(p => p.WarrantyCard).WithOptional(i => i.Product);
 
        //The second group (the two effects are exactly the same)
        HasRequired(p => p.WarrantyCard).WithRequiredPrincipal(i => i.Product);
        HasOptional(p => p.WarrantyCard).WithRequired(i => i.Product);
    }
}
 
public class WarrantyCardMap : EntityTypeConfiguration<WarrantyCard>
{
    public WarrantyCardMap()
    {
        ToTable("WarrantyCard");
        HasKey(i => i.ProductId);
    }
} 

In addition to these combinations, other combinations can not achieve the effect (they will generate redundant foreign keys).

The first set of migration codes generated by Fluent API:

CreateTable(
    "dbo.Product",
    c => new
        {
            Id = c.Int(nullable: false),
            Name = c.String(),
            Description = c.String(maxLength: 200),
        })
    .PrimaryKey(t => t.Id)
    .ForeignKey("dbo.WarrantyCard", t => t.Id)
    .Index(t => t.Id);
 
CreateTable(
    "dbo.WarrantyCard",
    c => new
        {
            ProductId = c.Int(nullable: false, identity: true),
            ExpiredDate = c.DateTime(nullable: false),
        })
    .PrimaryKey(t => t.ProductId); 

It is worth noting that the foreign key is specified in the Id column of the Product table, and the primary key Id of the Product is not used as the identification column.

Let's take a look at the migration code generated by the second group of fluent APIs:

CreateTable(
    "dbo.Product",
    c => new
        {
            Id = c.Int(nullable: false, identity: true),
            Name = c.String(),
            Description = c.String(maxLength: 200),
        })
    .PrimaryKey(t => t.Id);
 
CreateTable(
    "dbo.WarrantyCard",
    c => new
        {
            ProductId = c.Int(nullable: false),
            ExpiredDate = c.DateTime(nullable: false),
        })
    .PrimaryKey(t => t.ProductId)
    .ForeignKey("dbo.Product", t => t.ProductId)
    .Index(t => t.ProductId); 

The change is that the foreign key is added to the primary key ProductId of the warrentycard table, and this key is not used as an identification column.

Which of these two sets of configurations should be selected for the current scene. For products and warranty cards, there must be products before warranty cards. Warranty cards should depend on products.
Set the foreign key of warrycard to the second group of warrycard, which is more suitable for the current scenario.
That is, the Product is the Principal and the warranty card is the Dependent. In fact, so many codes are nothing more than clarifying the status of the two associated objects, the Principal and the Dependent.

After creating a table using the second set of configurations, we can add data:

Warranty card and certificate can be added at one time:

var product = new Product()
{
    Name = "air conditioner",
    Description = "Ice cold",
    WarrantyCard = new WarrantyCard()
    {
        ExpiredDate = DateTime.Now.AddYears(3)
    }
};
context.Set<Product>().Add(product);
context.SaveChanges(); 

It can also be carried out separately:

var product = new Product()
{
    Name = "Projector",
    Description = "high resolution"
};
context.Set<Product>().Add(product);
context.SaveChanges();
 
WarrantyCard card = new WarrantyCard()
{
    ProductId = product.Id,
    ExpiredDate = DateTime.Now.AddYears(3)
};
context.Set<WarrantyCard>().Add(card);
context.SaveChanges(); 

For queries, the SQL generated by the first group and the second group are the same. They are all internal joins, so they are not listed here.

One way 1 - * Association (can be empty)

The new roles here are and invoices. Invoices have their own numbers. Some products have invoices and some products have no invoices. We want to find the invoice through the product without associating the invoice with the product.

public class Invoice
{
    public int Id { get; set; }
    public string InvoiceNo { get; set; }   
    public DateTime CreateDate { get; set; }
} 

The new attributes of the product category are as follows:

public virtual Invoice Invoice { get; set; }
public int? InvoiceId { get; set; } 

You can use the following code to create an association between Product and Invoice

public class ProductMap : EntityTypeConfiguration<Product>
{
    public ProductMap()
    {
        ToTable("Product");
        HasKey(p => p.Id);
        HasOptional(p => p.Invoice).WithMany().HasForeignKey(p => p.InvoiceId);
    }
}
 
public class InvoiceMap : EntityTypeConfiguration<Invoice>
{
    public InvoiceMap()
    {
        ToTable("Invoice");
        HasKey(i => i.Id);
    }
} 

HasOptional indicates that a Product may have an invoice. If the WithMany parameter is empty, it means that we do not need to associate the invoice with the Product. The HasForeignKey is used to specify the foreign key column in the Product table.

You can also configure whether to cascade deletion through WillCascadeOnDelete(). We all know this, so we won't say more.

After running the migration, the foreign key of the Product table generated by the database can be empty (note that the attribute representing the foreign key in the entity class must be Nullable, otherwise the migration code cannot be generated).

The following code is written to test the mapping configuration. First, create a test object

var product = new Product()
{
    Name = "book",
    Description = "Coder books",
    Invoice = new Invoice()//It's OK not to create an Invoice here, because it can be null
    {
        InvoiceNo = "12345",
        CreateDate = DateTime.Now
    }
};
context.Set<Product>().Add(product);
context.SaveChanges(); 

Then query. Note that the creation and query should be executed twice, otherwise the database will not be used, and the results will be returned directly by EF Context.

var productGet = context.Set<Product>().Include(p=>p.Invoice).FirstOrDefault(); 

Through SS Profiler, you can see the generated SQL as follows:

SELECT TOP (1) 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Name] AS [Name], 
    [Extent1].[Description] AS [Description], 
    [Extent1].[InvoiceId] AS [InvoiceId], 
    [Extent2].[Id] AS [Id1], 
    [Extent2].[InvoiceNo] AS [InvoiceNo], 
    [Extent2].[CreateDate] AS [CreateDate]
    FROM  [dbo].[Products] AS [Extent1]
    LEFT OUTER JOIN [dbo].[Invoices] AS [Extent2] ON [Extent1].[InvoiceId] = [Extent2].[Id] 

We can see that for the case where foreign keys can be empty, the SQL generated by EF uses LEFT OUTER JOIN, which basically compounds our expectations.

One way 1 - * Association (cannot be empty)

To demonstrate this association, please issue a new object certificate. The certificate has its own number, and a product must have a certificate.

public class Certification
{
    public int Id { get; set; }
    public string Inspector { get; set; }
} 

We add the attribute of the associated certificate to the Product:

public virtual Certification Certification { get; set; }
public int CertificationId { get; set; } 

The code for configuring Product to Certification mapping is similar to the previous one, that is, HasOptional is replaced by HasRequired:

HasRequired(p => p.Certification).WithMany().HasForeignKey(p=>p.CertificationId); 

The foreign key column of the generated migration code cannot be empty. When creating an object, Product must be created together with Certification. The generated query statements are the same except for replacing LEFT OUTER JOIN with INNER JOIN,
No more details.

Bidirectional 1 - * Association

This is a common scenario. For example, a product can correspond to multiple photos, and each photo is associated with a product. Let's take a look at the new photo category:

public class ProductPhoto
{
    public int Id { get; set; }
    public string FileName { get; set; }
    public float FileSize { get; set; }
    public virtual Product Product { get; set; }
    public int ProductId { get; set; }
} 

Add ProductPhoto set to Product:

public virtual ICollection<ProductPhoto> Photos { get; set; } 

Then the mapping configuration:

public class ProductMap : EntityTypeConfiguration<Product>
{
    public ProductMap()
    {
        ToTable("Product");
        HasKey(p => p.Id);
        HasMany(p => p.Photos).WithRequired(pp => pp.Product).HasForeignKey(pp => pp.ProductId);
    }
}
 
public class ProductPhotoMap : EntityTypeConfiguration<ProductPhoto>
{
    public ProductPhotoMap()
    {
        ToTable("ProductPhoto");
        HasKey(pp => pp.Id);
    }
} 

The code is easy to understand. HasMany indicates that there are multiple productphotos in the Product, and WithRequired indicates that the ProductPhoto must be associated with a Product.

Let's look at another equivalent way to write (configure Association in ProductPhoto):

public class ProductMap : EntityTypeConfiguration<Product>
{
    public ProductMap()
    {
        ToTable("Product");
        HasKey(p => p.Id);
    }
}
 
public class ProductPhotoMap : EntityTypeConfiguration<ProductPhoto>
{
    public ProductPhotoMap()
    {
        ToTable("ProductPhoto");
        HasKey(pp => pp.Id);
        HasRequired(pp => pp.Product).WithMany(p => p.Photos).HasForeignKey(pp => pp.ProductId);
    }
} 

Does it feel like the previous one-way 1 - * configuration? In fact, WithMany has more parameters. With more and more examples, you should have a deeper understanding of these configurations.

After migrating to the database, we add some data to test:

var product = new Product()
{
    Name = "Projector",
    Description = "high resolution"
};
context.Set<Product>().Add(product);
context.SaveChanges();
 
ProductPhoto pp1 = new ProductPhoto()
{
    FileName = "Front view",
    FileSize = 3,
    ProductId = product.Id
};
 
ProductPhoto pp2 = new ProductPhoto()
{
    FileName = "profile",
    FileSize = 5,
    ProductId = product.Id
};
 
context.Set<ProductPhoto>().Add(pp1);
context.Set<ProductPhoto>().Add(pp2);
context.SaveChanges(); 

Try to read Product and ProductPhoto:

var productGet = context.Set<Product>().Include(p=>p.Photos).ToList(); 

The generated SQL is as follows:

SELECT
        [Limit1].[Id] AS [Id], 
        [Limit1].[Name] AS [Name], 
        [Limit1].[Description] AS [Description], 
        [Extent2].[Id] AS [Id1], 
        [Extent2].[FileName] AS [FileName], 
        [Extent2].[FileSize] AS [FileSize], 
        [Extent2].[ProductId] AS [ProductId], 
        CASE WHEN ([Extent2].[Id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
        FROM   (SELECT TOP (1) [c].[Id] AS [Id], [c].[Name] AS [Name], [c].[Description] AS [Description]
            FROM [dbo].[Product] AS [c] ) AS [Limit1]
        LEFT OUTER JOIN [dbo].[ProductPhoto] AS [Extent2] ON [Limit1].[Id] = [Extent2].[ProductId] 

It's a little complicated. The reason for using LEFT OUTER JOIN is that some products may not have ProductPhoto.

*- * Association

This time it's the product label's turn. A product can have multiple labels, and a label can also correspond to multiple products:

public class Tag
{
    public int Id { get; set; }
    public string Text { get; set; }
    public virtual ICollection<Product> Products { get; set; }
} 

Add tag set to Product:

public virtual ICollection<Tag> Tags { get; set; } 

Mapping code:

public class ProductMap : EntityTypeConfiguration<Product>
{
    public ProductMap()
    {
        ToTable("Product");
        HasKey(p => p.Id);
        HasMany(p => p.Tags).WithMany(t => t.Products).Map(m => m.ToTable("Product_Tag_Mapping"));
    }
}
 
public class TagMap : EntityTypeConfiguration<Tag>
{
    public TagMap()
    {
        ToTable("Tag");
        HasKey(t => t.Id);
    }
} 

The special thing is that you need to specify an association table to save the many to many mapping relationship.

CreateTable(
    "dbo.Product_Tag_Mapping",
    c => new
        {
            Product_Id = c.Int(nullable: false),
            Tag_Id = c.Int(nullable: false),
        })
    .PrimaryKey(t => new { t.Product_Id, t.Tag_Id })
    .ForeignKey("dbo.Product", t => t.Product_Id, cascadeDelete: true)
    .ForeignKey("dbo.Tag", t => t.Tag_Id, cascadeDelete: true)
    .Index(t => t.Product_Id)
    .Index(t => t.Tag_Id); 

In general, it is good to use the automatically generated foreign key, or you can define the foreign key name yourself.

HasMany(p => p.Tags).WithMany(t => t.Products).Map(m =>
{
    m.ToTable("Product_Tag_Mapping");
    m.MapLeftKey("Pid");
    m.MapRightKey("Tid");
}); 

The migration code becomes as follows:

CreateTable(
    "dbo.Product_Tag_Mapping",
    c => new
        {
            Pid = c.Int(nullable: false),
            Tid = c.Int(nullable: false),
        })
    .PrimaryKey(t => new { t.Pid, t.Tid })
    .ForeignKey("dbo.Product", t => t.Pid, cascadeDelete: true)
    .ForeignKey("dbo.Tag", t => t.Tid, cascadeDelete: true)
    .Index(t => t.Pid)
    .Index(t => t.Tid); 

Removing the WithMany parameter from the mapping code is a one-way * - * mapping effect. For example, we need to find all tags through Product, but we don't need to find the Product with this Tag through Tag. Somewhat similar to one-way 1 - *.

However, whether WithMany has parameters or not, the generated migration code is the same.

We also write some data in and test:

var product = new Product()
{
    Name = "Projector",
    Description = "high resolution",
    Tags = new List<Tag>
    {
        new Tag(){Text = "High cost performance"}
    }
     
};
context.Set<Product>().Add(product);
context.SaveChanges(); 

SQL when using preload (include (P = > p.tags)):

SELECT
    [Project1].[Id] AS [Id], 
    [Project1].[Name] AS [Name], 
    [Project1].[Description] AS [Description], 
    [Project1].[C1] AS [C1], 
    [Project1].[Id1] AS [Id1], 
    [Project1].[Text] AS [Text]
    FROM ( SELECT
        [Limit1].[Id] AS [Id], 
        [Limit1].[Name] AS [Name], 
        [Limit1].[Description] AS [Description], 
        [Join1].[Id] AS [Id1], 
        [Join1].[Text] AS [Text], 
        CASE WHEN ([Join1].[Product_Id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
        FROM   (SELECT TOP (1) [c].[Id] AS [Id], [c].[Name] AS [Name], [c].[Description] AS [Description]
            FROM [dbo].[Product] AS [c] ) AS [Limit1]
        LEFT OUTER JOIN  (SELECT [Extent2].[Product_Id] AS [Product_Id], [Extent3].[Id] AS [Id], [Extent3].[Text] AS [Text]
            FROM  [dbo].[Product_Tag_Mapping] AS [Extent2]
            INNER JOIN [dbo].[Tag] AS [Extent3] ON [Extent3].[Id] = [Extent2].[Tag_Id] ) AS [Join1] ON [Limit1].[Id] = [Join1].[Product_Id]
    )  AS [Project1]
    ORDER BY [Project1].[Id] ASC, [Project1].[C1] ASC 

As you might expect, because there are three tables, the JOIN is used twice.

A little supplement

In the previous example, the HasForeignKey() method is used many times to specify the foreign key. If there is no attribute representing the foreign key in the entity class, we can specify the foreign key column in the following way
In this way, this foreign key column only exists in the database, not in the entity:

HasOptional(p => p.Invoice).WithMany().Map(m => m.MapKey("DbOnlyInvoiceId")); 

EF provides many methods for association mapping, which can be described as dazzling. Only part of what I know is written above. If there are scenes that are not covered, you are welcome to discuss them in the comments.

Keywords: net ef

Added by pranav_kavi on Thu, 03 Feb 2022 17:09:59 +0200