7 Configuring nonrelational properties

This chapter covers

  • Configuring EF Core three ways
  • Focusing on nonrelational properties
  • Defining the database structure
  • Introducing value converters, shadow properties, and backing fields
  • Deciding which type of configuration works best in different situations

This chapter introduces configuring EF Core in general but concentrates on configuring the nonrelational properties in an entity class; these properties are known as scalar properties. Chapter 8 covers configuring relational properties, and chapter 10 covers configuring more-advanced features, such as DbFunctions, computed columns, and so on.

This chapter starts with an overview of the configuration process that EF Core runs when the application’s DbContext is used for the first time. Then you’ll learn how to configure the mapping between the .NET classes and their associated database tables, with features such as setting the name, SQL type, and nullability of the columns in a table.

This chapter also introduces three EF Core features—value converters, shadow properties, and backing fields —that enable you to control how the data is stored and controlled by the rest of your non-EF Core code. Value converters, for example, allow you to transform data when it is written/read from the database, allowing you to make the database representation easier to understand and debug; shadow properties and backing fields allow you to “hide,” or control access to, database data at the software level. These features can help you write better, less fragile applications that are easier to debug and refactor.

7.1 Three ways of configuring EF Core

Chapter 1 covered how EF Core models the database and presented a figure to show what EF Core is doing, with the focus on the database. Figure 7.1 has a more detailed depiction of the configuration process that happens the first time you use the application’s DbContext. This figure shows the entire process, with the three configuration approaches: By Convention, Data Annotations, and the Fluent API. This example focuses on the configuration of scalar properties, but the process is the same for all configurations of EF Core.

07_01

Figure 7.1 When the application’s DbContext is first used, EF Core sets off a process to configure itself and build a model of the database it’s supposed to access. You can use three approaches to configure EF Core: By Convention, Data Annotations, and Fluent API. Most real applications need a mixture of all three approaches to configure EF Core in exactly the way your application needs.

This list summarizes the three approaches to configuring EF Core:

  • By Convention —When you follow simple rules on property types and names, EF Core will autoconfigure many of the software and database features. The By Convention approach is quick and easy, but it can’t handle every eventuality.

  • Data Annotations —A range of .NET attributes known as Data Annotations can be added to entity classes and/or properties to provide extra configuration information. These attributes can also be useful for data validation, covered in chapter 4.

  • Fluent API —EF Core has a method called OnModelCreating that’s run when the EF context is first used. You can override this method and add commands, known as the Fluent API, to provide extra information to EF Core in its modeling stage. The Fluent API is the most comprehensive form of configuration information, and some features are available only via that API.

Note Most real applications need to use all three approaches to configure EF Core and the database in exactly the way they need. Some configuration features are available via two or even all three approaches (such as defining the primary key in an entity class). Section 7.16 gives you my recommendations on which approach to use for certain features, plus a way to automate some of your configurations.

7.2 A worked example of configuring EF Core

For anything beyond a Hello World version of using EF Core, you’re likely to need some form of Data Annotations or Fluent API configuration. In part 1, you needed to set up the key for the many-to-many link table. In this chapter, you’ll see an example of applying the three configuration approaches introduced in section 7.1 to better match the database to the needs of our Book App.

In this example, you’re going to remodel the Book entity class used in chapters 2-5 and change the size and type of some of the columns from the defaults that EF Core uses via a EF Core migration. These changes make your database smaller, make sorting or searching on some columns faster, and check that some columns aren’t null. It’s always good practice to define the correct size, type, and nullability for your database columns based on the business needs.

To do this, you’ll use a combination of all three configuration approaches. The By Convention configuration has a major part to play, as it defines the table and column names, but you’ll add specific Data Annotations and Fluent API configuration methods to change a few of the columns from the default By Convention settings. Figure 7.2 shows how each configuration approach affects EF Core’s internal model of database table structure. Because of space limitations, the figure doesn’t show all the Data Annotations and Fluent API configuration methods applied to the table, but you can see them in listings 7.1 and 7.2, respectively.

NOTE Figure 7.2 uses arrows to link different EF Core configuration code to the parts of the database table’s columns. To be completely clear, changing EF Core configurations doesn’t magically change the database. Chapter 9, which is about changing the database structure (known as the schema) covers several ways in which the EF Core configurations alter the database or the database alters the EF Core configurations in your code.

07_02

Figure 7.2 To configure the Books table in the exact format you want, you must use all three configuration approaches. A large part is done with By Convention (all the parts not in bold), but then you use Data Annotations to set the size and nullability of the Title column and the Fluent API to change the type of the PublishedOn and ImageUrl columns.

You will see more detailed explanations of these settings as you read this chapter, but this part gives you an overall view of different ways you can configure your application’s DbContext. It’s also interesting to think about how some of these configurations could be useful in your own projects. Here are a few EF Core configurations that I use in most projects I work on:

  • [Required] attribute —This attribute tells EF Core that the Title column can’t be SQL NULL, which means that the database will return an error if you try to insert/update a book with a null Title property.

  • [MaxLength(256)] attribute —This attribute tells EF Core that the number of characters stored in the database should 256 rather than defaulting to the database’s maximum size (2 GB in SQL Server). Having fixed-length strings of the right type, 2-byte Unicode or 1-byte ASCII, makes the database access slightly more efficient and allows an SQL index to be applied to these fixed-size columns.

Definition An SQL index is a feature that improves the performance of sorting and searching. Section 7.10 covers this topic in more detail.

  • HasColumnType("date") Fluent API —By making the PublishedOn column hold only the date (which is all you need) rather than the default datetime2, you reduce the column size from 8 bytes to 3 bytes, which makes searching and sorting on the PublishedOn column faster.

  • IsUnicode(false) Fluent API —The ImageUrl property contains only 8-bit ASCII characters, so you tell EF Core so, which means that the string will be stored that way. So if the ImageUrl property has a [MaxLength(512)] attribute (as shown in listing 7.1), the IsUnicode(false)method would reduce the size of the ImageUrl column from 1024 bytes (Unicode takes 2 bytes per character) to 512 bytes (ASCII takes 1 byte per character).

This listing shows you the updated Book entity class code, with the new Data Annotations in bold. (The Fluent API commands are described in section 7.5.)

Listing 7.1 The Book entity class with added Data Annotations

public class Book                        
{
    public int BookId { get; set; }
 
    [Required]                                
    [MaxLength(256)]                          
    public string Title { get; set; }
    public string Description { get; set; }
    public DateTime PublishedOn { get; set; }
    [MaxLength(64)]                           
    public string Publisher { get; set; }
    public decimal Price { get; set; }
 
    [MaxLength(512)]                          
    public string ImageUrl { get; set; }
    public bool SoftDeleted { get; set; }
 
    //-----------------------------------------------
    //relationships
 
    public PriceOffer Promotion { get; set; }         
    public IList<Review> Reviews { get; set; } 
    public IList<BookAuthor> AuthorsLink { get; set; }                    
}

Tells EF Core that the string is non-nullable

Defines the size of the string column in the database

TIP You’d normally set the size parameter in the [MaxLength(nn)] attribute by using a constant so that if you create a DTO, it will use the same constant. If you change the size of one property, you change all the associated properties.

Now that you’ve seen an example that uses all three configuration approaches, let’s explore each approach in detail.

7.3 Configuring by convention

By Convention is the default configuration, which can be overridden by the other two approaches, Data Annotations and the Fluent API. The By Convention approach relies on the developer to use the By Convention naming standards and type mappings, which allow EF Core to find and configure entity classes and their relationships, as well as define much of the database model. This approach provides a quick way to configure much of your database mapping, so it’s worth learning.

7.3.1 Conventions for entity classes

Classes that EF Core maps to the database are called entity classes. As stated in chapter 2, entity classes are normal .NET classes, sometimes referred to as POCOs (plain old CLR objects). EF Core requires entity classes to have the following features:

  • The class must be of public access: the keyword public should be before the class.

  • The class can’t be a static class, as EF Core must be able to create a new instance of the class.

  • The class must have a constructor that EF Core can use. The default, parameterless constructor works, and other constructors with parameters can work. See section 6.1.10 for the detailed rules on how EF Core uses constructors.

7.3.2 Conventions for parameters in an entity class

By convention, EF Core will look for public properties in an entity class that have a public getter and a setter of any access mode (public, internal, protected, or private). The typical, all-public property is

public int MyProp { get; set; }

Although the all-public property is the norm, in some places having a property with a more localized access setting (such as public int MyProp { get; private set; }) gives you more control of how it’s set. One example would be a method in the entity class that also does some checks before setting the property; see chapter 13 for more information.

NOTE EF Core can handle read-only properties—properties with only a getter, such as public int MyProp { get; }. But in that case, the By Convention approach won’t work; you need to use Fluent API to tell EF Core that those properties are mapped to the database.

7.3.3 Conventions for name, type, and size

Here are the rules for the name, type, and size of a relational column:

  • The name of the property is used as the name of the column in the table.

  • The .NET type is translated by the database provider to the corresponding SQL type. Many basic .NET types have a one-to-one mapping to a corresponding database type. These basic .NET types are mostly .NET primitive types (int, bool, and so on), with some special cases (such as string, DateTime, and Guid).

  • The size is defined by the .NET type; for instance, the 32-bit int type is stored in the corresponding SQL’s 32-bit INT type. String and byte[] types take on a size of max, which will be different for each database type.

EF6 One change in the default mapping conventions is that EF Core maps a .NET DateTime type to SQL datetime2(7), whereas EF6 maps .NET DateTime to SQL datetime. Microsoft recommends using datetime2(7) because it follows the ANSI and ISO SQL standard. Also, datetime2(7) is more accurate: SQL datetime’s resolution is about 0.004 seconds, whereas datetime2(7) has a resolution of 100 nanoseconds.

7.3.4 By convention, the nullability of a property is based on .NET type

In relational databases, NULL represents missing or unknown data. Whether a column can be NULL is defined by the .NET type:

  • If the type is string, the column can be NULL, because a string can be null.

  • Primitive types (such as int) or struct types (such as DateTime) are non-null by default.

  • Primitive or struct types can be made nullable by using either the ? suffix (such as int?) or the generic Nullable<T> (such as Nullable<int>). In these cases, the column can be NULL.

Figure 7.3 shows the name, type, size, and nullability conventions applied to a property.

07_03

Figure 7.3 The application of the By Convention rules to define an SQL column. The type of the property is converted by the database provider to the equivalent SQL type, whereas the name of the property is used for the name of the column.

7.3.5 An EF Core naming convention identifies primary keys

The other rule is about defining the database table’s primary key. The EF Core conventions for designating a primary key are as follows:

  • EF Core expects one primary-key property. (The By Convention approach doesn’t handle keys made up of multiple properties/columns, called composite keys.)

  • The property is called Id or <class name>id (such as BookId).

  • The type of the property defines what assigns a unique value to the key. Chapter 8 covers key generation.

Figure 7.4 shows an example of a database-generated primary key with By Convention mapping for the Book’s BookId property and the Books table’s SQL column BookId.

07_04

Figure 7.4 The mapping between the .NET class property BookId and the SQL primary column BookId, using the By Convention approach. The name of the property tells EF Core that this property is the primary key. Also, the database provider knows that a type of int means that it should create a unique value for each row added to the table.

TIP Although you have the option of using the short name, Id, for a primary key, I recommend that you use the longer name: <class name> followed by Id (BookId, for example). Understanding what’s going on in your code is easier if you use Where(p => BookId == 1) rather than the shorter Where(p => Id == 1), especially when you have lots of entity classes.

7.4 Configuring via Data Annotations

Data Annotations are a specific type of .NET attribute used for validation and database features. These attributes can be applied to an entity class or property and provide configuration information to EF Core. This section introduces where you can find them and how they’re typically applied. The Data Annotation attributes that are relevant to EF Core configuration come from two namespaces.

7.4.1 Using annotations from System.ComponentModel.DataAnnotations

The attributes in the System.ComponentModel.DataAnnotations namespace are used mainly for data validation at the frontend, such as ASP.NET, but EF Core uses some of them for creating the mapping model. Attributes such as [Required] and [MaxLength] are the main ones, with many of the other Data Annotations having no effect on EF Core. Figure 7.5 shows how the main attributes, [Required] and [MaxLength], affect the database column definition.

07_05

Figure 7.5 The [Required] and [MaxLength] attributes affect the mapping to a database column. The [Required] attribute indicates that the column shouldn’t be null, and the [MaxLength] attribute sets the size of the nvarchar.

7.4.2 Using annotations from System.ComponentModel.DataAnnotations.Schema

The attributes in the System.ComponentModel.DataAnnotations.Schema namespace are more specific to database configuration. This namespace was added in NET Framework 4.5, well before EF Core was written, but EF Core uses its attributes, such as [Table], [Column], and so on, to set the table name and column name/type, as described in section 7.11.

7.5 Configuring via the Fluent API

The third approach to configuring EF Core, called the Fluent API, is a set of methods that works on the ModelBuilder class that’s available in the OnModelCreating method inside your application’s DbContext. As you will see, the Fluent API works by extension methods that can be chained together, as LINQ commands are chained together, to set a configuration setting. The Fluent API provides the most comprehensive list of configuration commands, with many configurations available only via that API.

But before defining the Fluent API relationship commands, I want to introduce a different approach that segregates your Fluent API commands into per-entity class sized groups. This approach is useful because as your application grows, putting all Fluent API commands in the OnModelCreating method (as shown in figure 2.6) makes finding a specific Fluent API hard work. The solution is to move the Fluent API for an entity class into a separate configuration class that’s then called from the OnModelCreating method.

EF Core provides a method to facilitate this process in the shape of the IEntityTypeConfiguration<T> interface. Listing 7.2 shows your new application DbContext, EfCoreContext, where you move the Fluent API setup of the various classes into separate configuration classes. The benefit of this approach is that the Fluent API for an entity class is all in one place, not mixed with Fluent API commands for other entity classes.

EF6 EF6.x has an EntityTypeConfiguration<T> class that you can inherit to encapsulate the Fluent API configuration for a given entity class. EF Core’s implementation achieves the same result but uses an IEntityTypeConfiguration<T> interface that you apply to your configuration class.

Listing 7.2 Application’s DbContext for database with relationships

public class EfCoreContext : DbContext                             
{ 
    public EfCoreContext(DbContextOptions<EfCoreContext> options)  
        : base(options)                                            
    { }                                                            
 
    public DbSet<Book> Books { get; set; }                         
    public DbSet<Author> Authors { get; set; }                     
    public DbSet<PriceOffer> PriceOffers { get; set; }             
    public DbSet<Order> Orders { get; set; }                       
 
    protected override void                                        
        OnModelCreating(ModelBuilder modelBuilder)                 
    {                                               
        modelBuilder.ApplyConfiguration(new BookConfig());         
        modelBuilder.ApplyConfiguration(new BookAuthorConfig());   
        modelBuilder.ApplyConfiguration(new PriceOfferConfig());   
        modelBuilder.ApplyConfiguration(new LineItemConfig());     
    }
} 

UserId of the user who has bought some books

Creates the DbContext, using the options set up when you registered the DbContext

The entity classes that your code will access

The method in which your Fluent API commands run

Run each of the separate configurations for each entity class that needs configuration.

Let’s look at the BookConfig class used in listing 7.2 to see how you would construct a per-type configuration class. Listing 7.3 shows a configuration class that implements the IEntityTypeConfiguration<T> interface and contains the Fluent API methods for the Book entity class.

NOTE I am not describing the Fluent APIs in listing 7.3 because it is an example of the use of the IEntityTypeConfiguration<T> interface. The Fluent APIs are covered in section 7.7 (database type) and section 7.10 (indexes).

Listing 7.3 BookConfig extension class configures Book entity class

internal class BookConfig : IEntityTypeConfiguration<Book>
{
    public void Configure
        (EntityTypeBuilder<Book> entity)
    {
        entity.Property(p => p.PublishedOn)    
            .HasColumnType("date");            
 
        entity.Property(p => p.Price)          
            . HasPrecision(9,2);               
 
        entity.Property(x => x.ImageUrl)       
            .IsUnicode(false);                 
 
        entity.HasIndex(x => x.PublishedOn);   
    }
}

Convention-based mapping for .NET DateTime is SQL datetime2. This command changes the SQL column type to date, which holds only the date, not the time.

The precision of (9,2) sets a max price of 9,999,999.99 (9 digits, 2 after decimal point), which takes up the smallest size in the database.

The convention-based mapping for .NET string is SQL nvarchar (16 bit Unicode). This command changes the SQL column type to varchar (8-bit ASCII).

Adds an index to the PublishedOn property because you sort and filter on this property

In listing 7.2, I list each of the separate modelBuilder.ApplyConfiguration calls so that you can see them in action. But a time-saving method called ApplyConfigurationsFromAssembly can find all your configuration classes that inherit IEntityTypeConfiguration<T> and run them all for you. See the following code snippet, which finds and runs all your configuration classes in the same assembly as the DbContext:

modelBuilder.ApplyConfigurationsFromAssembly(
     Assembly.GetExecutingAssembly());

Listing 7.3 shows a typical use of the Fluent API, but please remember that the fluent nature of the API allows chaining of multiple commands, as shown in this code snippet:

modelBuilder.Entity<Book>()
    .Property(x => x.ImageUrl)
    .IsUnicode(false)
    .HasColumnName("DifferentName")
    .HasMaxLength(123)
    .IsRequired(false);

EF6 The Fluent API works the same in EF6.x, but with lots of new features and substantial changes in setting up relationships (covered in chapter 8) and subtle changes in data types.

OnModelCreating is called when the application first accesses the application’s DbContext. At that stage, EF Core configures itself by using all three approaches: By Convention, Data Annotations, and any Fluent API you’ve added in the OnModelCreating method.

What if Data Annotations and the Fluent API say different things?

The Data Annotations and the Fluent API modeling methods always override convention-based modeling. But what happens if a Data Annotation and the Fluent API both provide a mapping of the same property and setting?

I tried setting the SQL type and length of the WebUrl property to different values via Data Annotations and via the Fluent API. The Fluent API values were used. That test wasn’t a definitive one, but it makes sense that the Fluent API was the final arbitrator.

Now that you’ve learned about the Data Annotations and Fluent API configuration approaches, let’s detail the configuration of specific parts of the database model.

7.6 Excluding properties and classes from the database

Section 7.3.2 described how EF Core finds properties. But at times, you’ll want to exclude data in your entity classes from being in the database. You might want to have local data for a calculation used during the lifetime of the class instance, for example, but you don’t want it saved to the database. You can exclude a class or a property in two ways: via Data Annotations or via the Fluent API.

7.6.1 Excluding a class or property via Data Annotations

EF Core will exclude a property or a class that has a [NotMapped] data attribute applied to it. The following listing shows the application of the [NotMapped] data attribute to both a property and a class.

Listing 7.4 Excluding three properties, two by using [NotMapped]

public class MyEntityClass
{
    public int MyEntityClassId { get; set; }
 
    public string NormalProp{ get; set; }           
 
    [NotMapped]                                     
    public string LocalString { get; set; }
 
    public ExcludeClass LocalClass { get; set; }    
}
 
[NotMapped]                                         
public class ExcludeClass
{
    public int LocalInt { get; set; }
}

Included: A normal public property, with public getter and setter

Excluded: Placing a [NotMapped] attribute tells EF Core to not map this property to a column in the database.

Excluded: This class won’t be included in the database because the class definition has a [NotMapped] attribute on it.

Excluded: This class will be excluded because the class definition has a [NotMapped] attribute on it.

7.6.2 Excluding a class or property via the Fluent API

In addition, you can exclude properties and classes by using the Fluent API configuration command Ignore, as shown in listing 7.5.

Note For simplicity, I show the Fluent API inside the OnModelCreating method rather than in a separate configuration class.

Listing 7.5 Excluding a property and a class by using the Fluent API

public class ExcludeDbContext : DbContext
{
    public DbSet<MyEntityClass> MyEntities { get; set; }
    protected override void OnModelCreating
        (ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<MyEntityClass>()
            .Ignore(b => b.LocalString);       
        modelBuilder.Ignore<ExcludeClass>();   
    }
}

The Ignore method is used to exclude the LocalString property in the entity class, MyEntityClass, from being added to the database.

A different Ignore method can exclude a class such that if you have a property in an entity class of the Ignored type, that property isn’t added to the database.

As I said in section 7.3.2, by default, EF Core will ignore read-only properties—that is, a property with only a getter (such as public int MyProp { get; }).

7.7 Setting database column type, size, and nullability

As described earlier, the convention-based modeling uses default values for the SQL type, size/precision, and nullability based on the .NET type. A common requirement is to set one or more of these attributes manually, either because you’re using an existing database or because you have performance or business reasons to do so.

In the introduction to configuring (section 7.3), you worked through an example that changed the type and size of various columns. Table 7.1 provides a full list of the commands that are available to perform this task.

Table 7.1 Setting nullability and SQL type/size for a column

Setting

Data Annotations

Fluent API

Set not null(Default is nullable.)

[Required]public string MyProp { get; set; }

modelBuilder.Entity<MyClass>() .Property(p => p.MyProp) .IsRequired();

Set size (string)(Default is MAX length.)

[MaxLength(123)]public string MyProp { get; set; }

modelBuilder.Entity<MyClass>() .Property(p => p.MyProp) .HasMaxLength(123);

Set SQL type/size(Each type has a default precision and size.)

[Column(TypeName = "date")] public DateTime PublishedOn { get; set; }

modelBuilder.Entity<MyClass>( .Property(p => p.PublishedOn) .HasColumnType("date");

Some specific SQL types have their own Fluent API commands, which are shown in the following list. You can see the first Fluent API commands in use in listing 7.3:

  • IsUnicode(false)—Sets the SQL type to varchar(nnn) (1-byte character, known as ASCII) rather than the default of nvarchar(nnn) (2-byte character, known as Unicode).

  • HasPrecision(precision, scale)—Sets the number of digits (precision parameter) and how many of the digits are after the decimal point (scale parameter). This Fluent command is new in EF Core 5. The default setting of the SQL decimal is (18,2).

  • HasCollation(“collation name”)—Another EF Core 5 feature that allows you to define the collation on a property—that is, the sorting rules, case, and accent sensitivity properties of char and string types. (See section 2.8.3 for more about collations.)

I recommend using the IsUnicode(false) method to tell EF Core that a string property contains only single-byte ASCII-format characters, because using the IsUnicode method allows you to set the string size separately.

EF6 EF Core has a slightly different approach to setting the SQL data type of a column. If you provide the data type, you need to give the whole definition, both type and length/precision—as in [Column(TypeName = "varchar(nnn)")], where nnn is an integer number. In EF6, you can use [Column(TypeName = "varchar")] and then define the length by using [MaxLength(nnn)], but that technique doesn’t work in EF Core. See https://github.com/dotnet/ efcore/issues/3985 for more information.

7.8 Value conversions: Changing data to/from the database

EF Core’s value conversions feature allows you to change data when reading and writing a property to the database. Typical uses are

  • Saving Enum type properties as a string (instead of a number) so that it’s easier to understand when you’re looking at the data in the database

  • Fixing the problem of DateTime losing its UTC (Coordinated Universal Time) setting when read back from the database

  • (Advanced) Encrypting a property written to the database and decrypting on reading back

The value conversions have two parts:

  • Code that transforms the data as it is written out to the database

  • Code that transforms the database column back to the original type when read back

The first example of value conversions deals with a limitation of the SQL database in storing DateTime types, in that it doesn’t save the DateTimeKind part of the DateTime struct that tells us whether the DateTime is local time or UTC. This situation can cause problems. If you send that DateTime to your frontend using JSON, for example, the DateTime won’t contain the Z suffix character that tells JavaScript that the time is UTC, so your frontend code may display the wrong time. The following listing shows how to configure a property to have a value conversion that sets the DateTimeKind on the return from the database.

Listing 7.6 Configuring a DateTime property to replace the lost DateTimeKind setting

protected override void OnModelCreating
    (ModelBuilder modelBuilder)
{
    var utcConverter = new ValueConverter<DateTime, DateTime>(   
        toDb => toDb,                                            
        fromDb =>                                                
            DateTime.SpecifyKind(fromDb, DateTimeKind.Utc));     
 
    modelBuilder.Entity<ValueConversionExample>()                
        .Property(e => e.DateTimeUtcUtcOnReturn)                 
        .HasConversion(utcConverter);                            
    //... other configurations left out
}

Creates a ValueConverter from DateTime to DateTime

Saves the DateTime to the database in the normal way (such as no conversion)

On reading from the database, you add the UTC setting to the DateTime.

Selects the property you want to configure

Adds the utcConverter to that property

In this case, you had to create your own value converter, but about 20 built-in value converters are available. (See http://mng.bz/mgYP.) In fact, one value converter is so popular that it has a predefined Fluent API method or an attribute—a conversion to store an Enum as a string in the database. Let me explain.

Enums are normally stored in the database as numbers, which is an efficient format, but it does make things harder if you need to delve into the database to work out what happened. So some developers like to save Enums in the database as a string. You can configure a conversion of an Enum type to a string by using the HasConversion <string>() command, as in the following code snippet:

modelBuilder.Entity<ValueConversionExample>()
    .Property(e => e.Stage)
    .HasConversion<string>();

Following are some rules and limitations on using value conversions:

  • A null value will never be passed to a value converter. You need to write a value converter to handle only the non-null value, as your converter will be called only if the value isn’t a null.

  • Watch out for queries that contain sorting on a converted value. If you converted your Enums to a string, for example, the sorting will sort by the Enum name, not by the Enum value.

  • The converter can only map a single property to a single column in the database.

  • You can create some complex value converters, such as serializing a list of ints to a JSON string. At this point, EF Core cannot compare the List<int> property with the JSON in the database, so it won’t update the database. To solve this problem, you need to add what is called a value comparer. See the EF Core doc at http://mng.bz/5j5z for more information on this topic.

Later, in section 7.16.4, you will learn a way to automatically apply value converters to certain property types/names to make your life easier.

7.9 The different ways of configuring the primary key

You’ve already seen the By Convention approach of setting up the primary key of an entity. This section covers the normal primary-key setting—one key for which the .NET property defines the name and type. You need to configure the primary key explicitly in two situations:

  • When the key name doesn’t fit the By Convention naming rules

  • When the primary key is made up of more than one property/column, called a composite key

A many-to-many relationship-linking table is an example of where the By Convention approach doesn’t work. You can use two alternative approaches to define primary keys.

Note Chapter 8 deals with configuring foreign keys, because they define relationships even though they’re of a scalar type.

7.9.1 Configuring a primary key via Data Annotations

The [Key] attribute allows you to designate one property as the primary key in a class. Use this annotation when you don’t use the By Convention primary key name, as shown in the following listing. This code is simple and clearly marks the primary key.

Listing 7.7 Defining a property as the primary key by using the [Key] annotation

private class SomeEntity
{
    [Key]                                      
    public int NonStandardKeyName { get; set; }
 
    public string MyString { get; set; }
}

[Key] attribute tells EF Core that the property is a primary key.

Note that the [Key] attribute can’t be used for composite keys. In earlier versions of EF Core, you could define composite keys by using [Key] and [Column] attributes, but that feature has been removed.

7.9.2 Configuring a primary key via the Fluent API

You can also configure a primary key via the Fluent API, which is useful for primary keys that don’t fit the By Convention patterns. The following listing shows two primary keys being configured by the Fluent API’s HasKey method. The first primary key is a single primary key with a nonstandard name in the SomeEntity entity class, and the second is a composite primary key, consisting of two columns, in the BookAuthor linking table.

Listing 7.8 Using the Fluent API to configure primary keys on two entity classes

protected override void
    OnModelCreating(ModelBuilder modelBuilder)   
{                                                
    modelBuilder.Entity<SomeEntity>()
        .HasKey(x => x.NonStandardKeyName);          
 
    modelBuilder.Entity<BookAuthor>()          
        .HasKey(x => new {x.BookId, x.AuthorId});    
 
    //... other configuration settings removed
} 

Defines a normal, single-column primary key. Use HasKey when your key name doesn’t match the By Convention defaults.

Uses an anonymous object to define two (or more) properties to form a composite key. The order in which the properties appear in the anonymous object defines their order.

There is no By Convention version for composite keys, so you must use the Fluent API’s HasKey method.

7.9.3 Configuring an entity as read-only

In some advanced situations, your entity class might not have a primary key. Here are three examples:

  • You want to define an entity class as read-only. If an entity class hasn’t got a primary key, then EF Core will treat it as read-only.

  • You want to map an entity class to a read-only SQL View. SQL Views are SQL queries that work like SQL tables. See this article for more information: http://mng .bz/6g6y.

  • You want to map an entity class to an SQL query by using the ToSqlQuery Fluent API command. The ToSqlQuery method allows you to define an SQL command string that will be executed when you read in that entity class.

To set an entity class explicitly as read-only, you can use the fluent API HasNoKey() command or apply the attribute [Keyless] to the entity class. And if your entity class doesn’t have a primary key, you must mark it as read-only, using either of the two approaches. Any attempt to change the database via an entity class with no primary key will fail with an exception. EF Core does this because it can’t execute the update without a key, which is one way you can define an entity class as read-only. The other way to mark an entity as read-only is to map an entity to an SQL View by using the fluent API method ToView("ViewNameString") command, as shown in the following code snippet:

modelBuilder.Entity<MyEntityClass>()
    .ToView("MyView");

EF Core will throw an exception if you try to change the database via an entity class that is mapped to a View. If you want to map an entity class to an updatable view—an SQL View that can be updated—you should use the ToTable command instead.

7.10 Adding indexes to database columns

Relational databases have a feature called an index, which provides quicker searching and sorting of rows based on the column, or columns, in the index. In addition, an index may have a constraint, which ensures that each entry in the index is unique. A primary key is given a unique index, for example, to ensure that the primary key is different for each row in the table.

You can add an index to a column via Fluent API and attributes, as shown in table 7.2. An index will speed quick searching and sorting, and if you add the unique constraint, the database will ensure that the column value in each row will be different.

Table 7.2 Adding an index to a column

Action

Fluent API

Add index, Fluent

modelBuilder.Entity<MyClass>() .HasIndex(p => p.MyProp);

Add index, Attribute

[Index(nameof(MyProp))]public class MyClass ...

Add index, multiple columns

modelBuilder.Entity<Person>() .HasIndex(p => new {p.First, p.Surname});

Add index, multiple columns, Attribute

[Index(nameof(First), nameof(Surname)]public class MyClass ...

Add unique index, Fluent

modelBuilder.Entity<MyClass>() .HasIndex(p => p.BookISBN) .IsUnique();

Add unique index, Attribute

[Index(nameof(MyProp), IsUnique = true)]public class MyClass ...

Add named index, Fluent

modelBuilder.Entity<MyClass>() .HasIndex(p => p.MyProp) .HasDatabaseName("Index_MyProp");

Tip Don’t forget that you can chain the Fluent API commands together to mix and match these methods.

Some databases allow you to specify a filtered or partial index to ignore certain situations by using a WHERE clause. You could set a unique filtered index that ignored any soft-deleted items, for example. To set up a filtered index, you use the HasFilter Fluent API method containing an SQL expression to define whether the index should be updated with the value. The following code snippet gives an example of enforcing that the property MyProp will contain a unique value unless the SoftDeleted column of the table is true:

modelBuilder.Entity<MyClass>()
    .HasIndex(p => p.MyProp)
    .IsUnique()
    .HasFilter(“NOT SoftDeleted");

NOTE When you’re using the SQL Server provider, EF adds an IS NOT NULL filter for all nullable columns that are part of a unique index. You can override this convention by providing null to the HasFilter parameter—that is HasFilter(null).

7.11 Configuring the naming on the database side

If you’re building a new database, using the default names for the various parts of the database is fine. But if you have an existing database, or if your database needs to be accessed by an existing system you can’t change, you most likely need to use specific names for the schema name, the table names, and the column names of the database.

Definition Schema refers to the organization of data inside a database—the way the data is organized as tables, columns, constraints, and so on. In some databases, such as SQL Server, schema is also used to give a namespace to a particular grouping of data that the database designer uses to partition the database into logical groups.

7.11.1 Configuring table names

By convention, the name of a table is set by the name of the DbSet<T> property in the application’s DbContext, or if no DbSet<T> property is defined, the table uses the class name. In the application’s DbContext of our Book App, for example, you defined a DbSet<Book> Books property, so the database table name is set to Books. Conversely, you haven’t defined a DbSet<T> property for the Review entity class in the application’s DbContext, so its table name used the class name and is, therefore, Review.

If your database has specific table names that don’t fit the By Convention naming rules—for example, if the table name can’t be converted to a valid .NET variable name because it has a space in it—you can use either Data Annotations or the Fluent API to set the table name specifically. Table 7.3 summarizes the two approaches to setting the table name.

Table 7.3 Two ways to configure a table name explicitly for an entity class

Configuration method

Example: Setting the table name of the Book class to "XXX"

Data Annotations

[Table("XXX")]public class Book ... etc.

Fluent API

modelBuilder.Entity<Book>().ToTable("XXX");

7.11.2 Configuring the schema name and schema groupings

Some databases, such as SQL Server, allow you to group your tables by using what is called a schema name. You could have two tables with the same name but different schema names: a table called Books with a schema name Display, for example, would be different from a table called Books with a schema name Order.

By convention, the schema name is set by the database provider because some databases, such as SQLite and MySQL, don’t support schemas. In the case of SQL Server, which does support schemas, the default schema name is dbo, which is the SQL Server default name. You can change the default schema name only via the Fluent API, using the following snippet in the OnModelCreating method of your application’s DbContext:

modelBuilder.HasDefaultSchema("NewSchemaName");

Table 7.4 shows how to set the schema name for a table. You use this approach if your database is split into logical groups such as sales, production, accounts, and so on, and a table needs to be specifically assigned to a schema.

Table 7.4 Setting the schema name on a specific table

Configuration method

Example: Setting the schema name "sales" on a table

Data Annotations

[Table("SpecialOrder", Schema = "sales")] class MyClass ... etc.

Fluent API

modelBuilder.Entity<MyClass>() .ToTable("SpecialOrder", schema: "sales");

7.11.3 Configuring the database column names in a table

By convention, the column in a table has the same name as the property name. If your database has a name that can’t be represented as a valid .NET variable name or doesn’t fit the software use, you can set the column names by using Data Annotations or the Fluent API. Table 7.5 shows the two approaches.

Table 7.5 The two ways to configure a column name

Configuration method

Setting the column name of the BookId property to SpecialCol

Data Annotations

[Column("SpecialCol")]public int BookId { get; set; }

Fluent API

modelBuilder.Entity<MyClass>() .Property(b => b.BookId) .HasColumnName("SpecialCol");

7.12 Configuring Global Query Filters

Many applications, such as ASP.NET Core, have security features that control what views and controls the user can access. EF Core has a similar security feature called Global Query Filters (shortened to Query Filters). You can use Query Filters to build a multitenant application. This type of application holds data for different users in one database, but each user can see only the data they are allowed to access. Another use is to implement a soft-delete feature; instead of deleting data in the database, you might use a Query Filter to make the soft-deleted row disappear, but the data will still be there if you need to undelete it later.

I have found Query Filters to be useful in many client jobs, so I included a detailed section called “Using Global Query Filters in real-world situations” in chapter 6 (section 6.1.6). That section contains information on how to configure Query Filters, so please look there for that information. In section 7.16.4 of this chapter, I show how you can automate the configuration of Query Filters, which ensures that you won’t forget to add an important Query Filter to one of your entity classes.

7.13 Applying Fluent API commands based on the database provider type

The EF Core database providers provide a way to detect what database provider is being used when an instance of an application DbContext is created. This approach is useful for situations such as using, say, an SQLite database for your unit tests, but the production database is on an SQL Server, and you want to change some things to make your unit tests work.

SQLite, for example, doesn’t fully support a few NET types, such as decimal, so if you try to sort on a decimal property in an SQLite database, you’ll get an exception saying that you won’t get the right result from an SQLite database. One way to get around this issue is to convert the decimal type to a double type when using SQLite; it won’t be accurate, but it might be OK for a controlled set of unit tests.

Each database provider provides an extension method to return true if the database matches that provider. The SQL Server database provider, for example, has a method called IsSqlServer(); the SQLite database provider has a method called IsSqlite(); and so on. Another approach is to use the ActiveProvider property in the ModelBuilder class, which returns a string that is the NuGet package name of the database provider, such as "Microsoft.EntityFrameworkCore.SqlServer".

The following listing is an example of applying the decimal to double type change if the database is SQLite. This code allows the Book App’s OrderBooksBy query object method to use an in-memory SQLite database.

Listing 7.9 Using database-provider commands to set a column name

protected override void OnModelCreating
ModelBuilder modelBuilder)
{
    //... put your normal configration here
    if (Database.IsSqlite())               
    {                                     
        modelBuilder.Entity<Book>()        
            .Property(e => e.Price)        
            .HasConversion<double>();      
        modelBuilder.Entity<PriceOffer>()  
            .Property(e => e.NewPrice)     
            .HasConversion<double>();      
    }
}

The IsSqlite will return true if the database provided in the options is SQLite.

You set the two decimal values to double so that a unit test that sorts on these values doesn’t throw an exception.

EF Core 5 added the IsRelational() method, which returns false for database providers that aren’t relational, such as Cosmos Db. You can find a few database-specific Fluent API commands, such as the SQL Server provider method IsMemoryOptimized, in the EF Core documentation for each database provider.

NOte Although you could use this approach to create migrations for different production database types, it’s not recommended. The EF Core team suggests that you create a migration for each database type and store each migration in separate directories. For more information, see chapter 9.

7.14 Shadow properties: Hiding column data inside EF Core

EF6 EF6.x had the concept of shadow properties, but they were used only internally to handle missing foreign keys. In EF Core, shadow properties become a proper feature that you can use.

Shadow properties allow you to access database columns without having them appear in the entity class as a property. Shadow properties allow you to “hide” data that you consider not to be part of the normal use of the entity class. This is all about good software practice: you let upper layers access only the data they need, and you hide anything that those layers don’t need to know about. Let me give you two examples that show when you might use shadow properties:

  • A common need is to track by whom and when data was changed, maybe for auditing purposes or to understand customer behavior. The tracking data you receive is separate from the primary use of the class, so you may decide to implement that data by using shadow properties, which can be picked up outside the entity class.

  • When you’re setting up relationships in which you don’t define the foreign-key properties in your entity class, EF Core must add those properties to make the relationship work, and it does this via shadow properties. Chapter 8 covers this topic.

7.14.1 Configuring shadow properties

There’s a By Convention approach to configuring shadow properties, but because it relates only to relationships, I explain it in chapter 8. The other method is to use the Fluent API. You can introduce a new property by using the Fluent API method Property<T>. Because you’re setting up a shadow property, there won’t be a property of that name in the entity class, so you need to use the Fluent API’s Property<T> method, which takes a .NET Type and the name of the shadow property. The following listing shows the setup of a shadow property called UpdatedOn that’s of type DateTime.

Listing 7.10 Creating the UpdatedOn shadow property by using the Fluent API

public class Chapter06DbContext : DbContext
{
    ...
 
    protected override void 
        OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<MyEntityClass>()
            .Property<DateTime>("UpdatedOn");     
    ...
    }
}

Uses the Property<T> method to define the shadow property type

Under By Convention, the name of the table column the shadow property is mapped to is the same as the name of the shadow property. You can override this setting by adding the HasColumnName method on to the end of the property method.

Warning If a property of that name already exists in the entity class, the configuration will use that property instead of creating a shadow property.

7.14.2 Accessing shadow properties

Because the shadow properties don’t map to a class property, you need to access them directly via EF Core. For this purpose, you have to use the EF Core command Entry(myEntity).Property("MyPropertyName").CurrentValue, which is a read/write property, as shown in the following listing.

Listing 7.11 Using Entry(inst).Property(name) to set the shadow property

var entity = new SomeEntityClass();         
context.Add(entity);                        
context.Entry(entity)                       
    .Property("UpdatedOn").CurrentValue     
        = DateTime.Now;                     
context.SaveChanges();                      

Creates an entity class ...

... and adds it to the context, so it’s now tracked

Gets the EntityEntry from the tracked entity data

Uses the Property method to get the shadow property with read/write access

Sets that property to the value you want

Calls SaveChanges to save the MyEntityClass instance, with its normal and shadow property values, to the database

If you want to read a shadow property in an entity that has been loaded, use the context.Entry(entityInstance).Property("PropertyName").CurrentValue command. But you must read the entity as a tracked entity; you should read the entity without the AsNoTracking method being used in the query. The Entry(<entityInstance>).Property method uses the tracked entity data inside EF Core to hold the value, as it’s not held in the entity class instance.

In LINQ queries, you use another technique to access a shadow property: the EF.Property command. You could sort by the UpdatedOn shadow property, for example, by using the following query snippet, with the EF.Property method in bold:

context.MyEntities
    .OrderBy(b => EF.Property<DateTime>(b, "UpdatedOn"))
    .ToList();

7.15 Backing fields: Controlling access to data in an entity class

EF6 Backing fields aren’t available in EF6. This EF Core feature provides a level of control over access to data that EF6.x users have been after for some time.

As you saw earlier, columns in a database table are normally mapped to an entity class property with normal getters and setters—public int MyProp { get ; set; }. But you can also map a private field to your database. This feature is called a backing field, and it gives you more control of the way that database data is read or set by the software.

Like shadow properties, backing fields hide data, but they do the hiding in another way. For shadow properties, the data is hidden inside EF Core’s data, but backing fields hide the data inside the entity class, so it’s easier for the entity class to access the backing field inside the class. Here are some examples of situations in which you might use backing fields:

  • Hiding sensitive data —Hiding a person’s date of birth in a private field and making their age in years available to the rest of the software.

  • Catching changes —Detecting an update of a property by storing the data in a private field and adding code in the setter to detect the update of a property. You will use this technique in chapter 12, when you use property change to trigger an event.

  • Creating Domain-Driven Design (DDD) entity classes —Creating DDD entity classes in which all the entity classes’ properties need to be read-only. Backing fields allow you to lock down navigational collection properties, as described in section 8.7.

But before you get into the complex versions, let’s start with the simplest form of backing fields, in which a property getter/setter accesses the field.

7.15.1 Creating a simple backing field accessed by a read/write property

The following code snippet shows you a string property called MyProperty, in which the string data is stored in a private field. This form of backing field doesn’t do anything particularly different from using a normal property, but this example shows the concept of a property linked to a private field:

public class MyClass
{
    private string _myProperty;
    public string MyProperty 
    {
        get { return _myProperty; }
        set { _myProperty = value; } 
    }
}

EF Core’s By Convention configuration will find the type of backing field and configure it as a backing field (see section 7.15.4 for backing-field configuration options), and by default, EF Core will read/write the database data to this private field.

7.15.2 Creating a read-only column

Creating a read-only column is the most obvious use, although it can also be implemented via a private setting property (see section 7.3.2). If you have a column in the database that you need to read but don’t want the software to write, a backing field is a great solution. In this case, you can create a private field and use a public property, with a getter only, to retrieve the value. The following code snippet gives you an example:

public class MyClass
{
    private string _readOnlyCol;
    public string ReadOnlyCol => _readOnlyCol;
}

Something must set the column property, such as setting a default value in the database column (covered in chapter 9) or through some sort of internal database method.

7.15.3 Concealing a person’s date of birth: Hiding data inside a class

Hiding a person’s date of birth is a possible use of backing fields. In this case, you deem for security reasons that a person’s date of birth can be set, but only their age can be read from the entity class. The following listing shows how to do this in the Person class by using a private _dateOfBirth field and then providing a method to set it and a property to calculate the person’s age.

Listing 7.12 Using a backing field to hide sensitive data from normal access

public class Person
{
    private DateTime _dateOfBirth;                     
 
    public void SetDateOfBirth(DateTime dateOfBirth)   
    {
        _dateOfBirth = dateOfBirth;
    }
 
    public int AgeYears =>                             
        Years(_dateOfBirth, DateTime.Today);
 
    //Thanks to dana on stackoverflow
    //see http://stackoverflow.com/a/4127477/1434764
    private static int Years(DateTime start, DateTime end)
    {
        return (end.Year - start.Year - 1) +
               (((end.Month > start.Month) ||
                 ((end.Month == start.Month)
                  && (end.Day >= start.Day)))
                   ? 1 : 0);
    }}

The private backing field, which can’t be accessed directly via normal .NET software

Allows the backing field to be set

You can access the person’s age but not their exact date of birth.

Note In the preceding example, you need to use the Fluent API to create a backing-field-only variable (covered in section 7.15.2), because EF Core can’t find this backing field by using the By Convention approach.

From the class point of view, the _dateOfBirth field is hidden, but you can still access the table column via various EF Core commands in the same way that you accessed the shadow properties: by using the EF.Property<DateTime>(entity, "_dateOfBirth") method.

The backing field, _dateOfBirth, isn’t totally secure from the developer, but that’s not the aim. The idea is to remove the date-of-birth data from the normal properties so that it doesn’t get displayed unintentionally in any user-visible view.

7.15.4 Configuring backing fields

Having seen backing fields in action, you can configure them By Convention, via Fluent API, and now in EF Core 5 via Data Annotations. The By Convention approach works well but relies on the class to have a property that matches a field by type and a naming convention. If a field doesn’t match the property name/type or doesn’t have a matching property such as in the _dateOfBirth example, you need to configure your backing fields with Data Annotations or by using the Fluent API. The following sections describe the various configuration approaches.

Configuring backing fields By Convention

If your backing field is linked to a valid property (see section 7.3.2), the field can be configured by convention. The rules for By Convention configuration state that the private field must have one of the following names that match a property in the same class:

  • _<property name> (for example, _MyProperty)

  • _<camel-cased property name > (for example, _myProperty)

  • m_<property name> (for example, m_MyProperty)

  • m_<camel-cased property name> (for example, m_myProperty)

Definition Camel case is a convention in which a variable name starts with a lowercase letter but uses an uppercase letter to start each subsequent word in the name—as in thisIsCamelCase.

Configuring backing fields via Data Annotations

New in EF Core 5 is the BackingField attribute, which allows you to link a property to a private field in the entity class. This attribute is useful if you aren’t using the By Convention backing field naming style, as in this example:

private string _fieldName;
[BackingField(nameof(_fieldName))]
public string PropertyName
{
    get { return _fieldName; }
}
 
public void SetPropertyNameValue(string someString)
{
    _fieldName = someString;
}

Configuring backing fields via the Fluent API

You have several ways of configuring backing fields via the Fluent API. We’ll start with the simplest and work up to the more complex. Each example shows you the OnModelCreating method inside the application’s DbContext, with only the field part being configured:

  • Setting the name of the backing field —If your backing field name doesn’t follow EF Core’s conventions, you need to specify the field name via the Fluent API. Here’s an example:

protected override void OnModelCreating
    (ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>()
        .Property(b => b.MyProperty)
        .HasField("_differentName");
    ...
} 
  • Supplying only the field name —In this case, if there’s a property with the correct name, by convention EF Core will refer to the property, and the property name will be used for the database column. Here’s an example:

protected override void OnModelCreating
    (ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>()
        .Property("_dateOfBirth")
        .HasColumnName("DateOfBirth");
    ...
} 

If no property getter or setter is found, the field will still be mapped to the column, using its name, which in this example is _dateOfBirth, but that’s most likely not the name you want for the column. So you add the HasColumnName Fluent API method to get a better column name. The downside is that you’d still need to refer to the data in a query by its field name (in this case, _dateOfBirth), which isn’t too friendly or obvious.

Advanced: Configuring how data is read/written to the backing field

Since the release of EF Core 3, the default database access mode for backing fields is for EF Core to read and write to the field. This mode works in nearly all cases, but if you want to change the database access mode, you can do so via the Fluent API UsePropertyAccessMode method. The following code snippet tells EF Core to try to use the property for read/write, but if the property is missing a setter, EF Core will fill in the field on a database read:

protected override void 
    OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>()
        .Property(b => b.MyProperty)
                .HasField("_differentName")
        .UsePropertyAccessMode(PropertyAccessMode.PreferProperty);
    ...
} 

TIP To see the various access modes for a backing field, use Visual Studio’s intellisense feature to look at the comments on each of the PropertyAccessMode Enum values.

7.16 Recommendations for using EF Core’s configuration

You have so many ways to configure EF Core, some of which duplicate each other, that it isn’t always obvious which of the three approaches you should use for each part of the configuration. Here are suggested approaches to use for each part of EF Core configuration:

  • Start by using the By Convention approach wherever possible, because it’s quick and easy.

  • Use the validation attributes—MaxLength, Required, and so on—from the Data Annotations approach, as they’re useful for validation.

  • For everything else, use the Fluent API approach, because it has the most comprehensive set of commands. But consider writing code to automate common settings, such as applying the DateTime “UTC fix” to all DateTime properties whose Name ends with "Utc".

The following sections provide more-detailed explanations of my recommendations for configuring EF Core.

7.16.1 Use By Convention configuration first

EF Core does a respectable job of configuring most standard properties, so always start with that approach. In part 1, you built the whole of this initial database by using the By Convention approach, apart from the composite key in the BookAuthor many-to-many linking entity class.

The By Convention approach is quick and easy. You’ll see in chapter 8 that most relationships can be set up purely by using the By Convention naming rules, which can save you a lot of time. Learning what By Convention can configure will dramatically reduce the amount of configuration code you need to write.

7.16.2 Use validation Data Annotations wherever possible

Although you can do things such as limit the size of a string property with either Data Annotations or the Fluent API, I recommend using Data Annotations for the following reasons:

  • Frontend validation can use them. Although EF Core doesn’t validate the entity class before saving it to the database, other parts of the system may use Data Annotations for validation. ASP.NET Core uses Data Annotations to validate input, for example, so if you input directly into an entity class, the validation attributes will be useful. Or if you use separate ASP.NET ViewModel or DTO classes, you can cut and paste the properties with their validation attributes.

  • You may want to add validation to EF Core’s SaveChanges. Using data validation to move checks out of your business logic can make your business logic simpler. Chapter 4 showed you how to add validation of entity classes when SaveChanges is called.

  • Data Annotations make great comments. Attributes, which include Data Annotations, are compile-time constants; they’re easy to see and easy to understand.

7.16.3 Use the Fluent API for anything else

Typically, I use the Fluent API for setting up the database column mapping (column name, column data type, and so on) when it differs from the conventional values. You could use the schema Data Annotations to do that, but I try to hide things like these inside the OnModelCreating method because they’re database implementation issues rather than software structure issues. That practice is more a preference than a rule, though, so make your own decision. Section 7.16.4 describes how to automate some of your Fluent API configurations, which saves you time and also ensures that all your configuration rules are applied to every matching class/property.

7.16.4 Automate adding Fluent API commands by class/property signatures

One useful feature of the Fluent API commands allows you to write code to find and configure certain configurations based on the class/property type, name, and so on. In a real application, you might have hundreds of DateTime properties that need the UTC fix you used in listing 7.6. Rather than add the configuration for each property by hand, wouldn’t it be nice to find each property that needs the UTC fix and apply it automatically? You’re going to do exactly that.

Automating finding/adding configurations relies on a type called IMutableModel, which you can access in the OnModelCreating method. This type gives you access to all the classes mapped by EF Core to the database, and each IMutableEntityType allows you to access the properties. Most configuration options can be applied via methods in these two interfaces, but a few, such as Query Filters, need a bit more work.

To start, you will build the code that will iterate through each entity class and its properties, and add one configuration, as shown in listing 7.13. This iteration approach defines the way to automate configurations, and in later examples, you will add extra commands to do more configurations.

The following example adds a value converter to a DateTime that applies the UTC fix shown in listing 7.6. But in the following listing, the UTC fix value converter is applied to every property that is a DateTime with a Name that ends with "Utc".

Listing 7.13 Applying value converter to any DateTime property ending in "Utc"

protected override void                                              
OnModelCreating(ModelBuilder modelBuilder)                           
{
    var utcConverter = new ValueConverter<DateTime, DateTime>(       
        toDb => toDb,                                                
        fromDb =>                                                    
            DateTime.SpecifyKind(fromDb, DateTimeKind.Utc));         
 
    foreach (var entityType in modelBuilder.Model.GetEntityTypes())  
    {
        foreach (var entityProperty in entityType.GetProperties())   
        {
            if (entityProperty.ClrType == typeof(DateTime)           
                && entityProperty.Name.EndsWith("Utc"))              
            {                                                        
                entityProperty.SetValueConverter(utcConverter);      
            }                                                        
            //... other examples left out for clarity
        }
    }
    //... rest of configration code left out

The Fluent API commands are applied in the OnModelCreating method.

Defines a value converter to set the UTC setting to the returned DateTime

Loops through all the classes that EF Core has currently found mapped to the database

Loops through all the properties in an entity class that are mapped to the database

Adds the UTC value converter to properties of type DateTime and Name ending in “Utc”

Listing 7.13 showed the setup of only one Type/Named property, but normally, you would have lots of Fluent API settings. In this example, you are going to do the following:

  1. Add the UTC fix value converter to properties of type DateTime whose Names end with "Utc".

  2. Set the decimal precision/scale where the property’s Name contains "Price".

  3. Set any string properties whose Name ends in "Url" to be stored as ASCII—that is, varchar(nnn).

The following code snippet shows the code inside the OnModelCreating method in the Book App DbContext to add these three configuration settings:

foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
    foreach (var entityProperty in entityType.GetProperties()) 
    {
        if (entityProperty.ClrType == typeof(DateTime)         
            && entityProperty.Name.EndsWith("Utc"))            
        {                                                      
            entityProperty.SetValueConverter(utcConverter);    
        }                                                      
 
        if (entityProperty.ClrType == typeof(decimal)          
            && entityProperty.Name.Contains("Price"))          
        {                                                      
            entityProperty.SetPrecision(9);                    
            entityProperty.SetScale(2);                        
        }                                                      
 
        if (entityProperty.ClrType == typeof(string)           
            && entityProperty.Name.EndsWith("Url"))            
        {                                                      
            entityProperty.SetIsUnicode(false);                
        }                                                     
    }
}

A few Fluent APIs configurations need class-specific code, however. The Query Filters, for example, need a query that accesses entity classes. For this case, you need to add an interface to the entity class you want to add a Query Filter to and create the correct filter query dynamically.

As an example, you are going to build code that allows you to add automatically the SoftDelete Query Filter described in section 3.5.1 and the UserId Query Filter shown in section 6.1.7. Of these two Query Filters, UserId is more complex because it needs to get the current UserId, which changes on every instance of the Book App’s DbContext. You can do this in a couple of ways, but you decide to provide the current instance of the DbContext to the query. The following listing shows the extension class, called SoftDeleteQueryExtensions, with its MyQueryFilterTypes enum.

Listing 7.14 The enum/class to use to set up Query Filters on every compatible class

public enum MyQueryFilterTypes { SoftDelete, UserId }               
 
public static class SoftDeleteQueryExtensions                       
{
    public static void AddSoftDeleteQueryFilter(                    
        this IMutableEntityType entityData,                         
        MyQueryFilterTypes queryFilterType,                         
        IUserId userIdProvider = null)                              
    {
        var methodName = $"Get{queryFilterType}Filter";             
        var methodToCall = typeof(SoftDeleteQueryExtensions)        
            .GetMethod(methodName,                                  
                BindingFlags.NonPublic | BindingFlags.Static)       
            .MakeGenericMethod(entityData.ClrType);                 
        var filter = methodToCall                                   
            .Invoke(null, new object[] { userIdProvider });         
        entityData.SetQueryFilter((LambdaExpression)filter);        
        if (queryFilterType == MyQueryFilterTypes.SoftDelete)       
            entityData.AddIndex(entityData.FindProperty(            
                nameof(ISoftDelete.SoftDeleted)));                  
        if (queryFilterType == MyQueryFilterTypes.UserId)           
            entityData.AddIndex(entityData.FindProperty(            
                nameof(IUserId.UserId)));                           
    }
 
    private static LambdaExpression GetUserIdFilter<TEntity>(       
        IUserId userIdProvider)                                     
        where TEntity : class, IUserId                              
    {                                                               
        Expression<Func<TEntity, bool>> filter =                    
            x => x.UserId == userIdProvider.UserId;                 
        return filter;                                              
    }                                                               
 
    private static LambdaExpression GetSoftDeleteFilter<TEntity>(   
        IUserId userIdProvider)                                     
        where TEntity : class, ISoftDelete                          
    {                                                               
        Expression<Func<TEntity, bool>> filter =                    
            x => !x.SoftDeleted;                                    
        return filter;                                              
    }
}

Defines the different type of LINQ query to put in the Query Filter

A static extension class

Call this method to set up the query filter.

First parameter comes from EF Core and allows you to add a query filter

Second parameter allows you to pick which type of query filter to add

Third optional property holds a copy of the current DbContext instance so that the UserId will be the current one

Creates the correctly typed method to create the Where LINQ expression to use in the Query Filter

Uses the filter returned by the created type method in the SetQueryFilter method

Adds an index on the SoftDeleted property for better performance

Adds an index on the UserId property for better performance

Creates a query that is true only if the _userId matches the UserID in the entity class

Creates a query that is true only if the SoftDeleted property is false

Because every query of an entity that has a Query Filter will contain a filter on that property, the code automatically adds an index on every property that is used in a Query Filter. That technique improves performance on that entity. Finally, the following listing shows how to use the code shown in listing 7.14 within the Book App’s DbContext to automate the configuration of the Query Filters.

Listing 7.15 Adding code to the DbContext to automate setting up Query Filters

public class EfCoreContext : DbContext, IUserId                         
{
    public Guid UserId { get; private set; }                            
 
    public EfCoreContext(DbContextOptions<EfCoreContext> options,       
        IUserIdService userIdService = null)                            
        : base(options)                                                 
    {                                                                   
        UserId = userIdService?.GetUserId()                             
                 ?? new ReplacementUserIdService().GetUserId();         
    }
 
    //DbSets removed for clarity
 
    protected override void                                             
        OnModelCreating(ModelBuilder modelBuilder)                      
    {
        //other configration code removed for clarity
 
        foreach (var entityType in modelBuilder.Model.GetEntityTypes()  
        {
            //other property code removed for clarity
 
            if (typeof(ISoftDelete)                                     
                .IsAssignableFrom(entityType.ClrType))                  
            {
                entityType.AddSoftDeleteQueryFilter(                    
                    MyQueryFilterTypes.SoftDelete);                     
            }
            if (typeof(IUserId)                                         
                .IsAssignableFrom(entityType.ClrType))                  
            {
                entityType.AddSoftDeleteQueryFilter(                    
                    MyQueryFilterTypes.UserId, this);                   
            }
        }
}

Adding the IUserId to the DbContext means that we can pass the DbContext to the UserId query filter.

Holds the UserId, which is used in the Query Filter that uses the IUserId interface

Sets up the UserId. If the userIdService is null, or if it returns null for the UserId, we set a replacement UserId.

The automate code goes in the OnModelCreating method.

Loops through all the classes that EF Core has currently found mapped to the database

If the class inherits the ISoftDelete interface, it needs the SoftDelete Query Filter.

Adds a Query Filter to this class, with a query suitable for SoftDelete

If the class inherits the IUserId interface, it needs the IUserId Query Filter.

Adds the UserId Query Filter to this class. Passing ‘this’ allows access to the current UserId.

For the Book App, all this automation is overkill, but in bigger applications, it can save you a great deal of time; more important, it ensures that you have set everything up correctly. To end this section, here are some recommendations and limitations that you should know about if you are going to use this approach:

  • If you run the automatic Fluent API code before your handcoded configurations, your handcoded configurations will override any of the automatic Fluent API settings. But be aware that if there is an entity class that is registered only via manually written Fluent API, that entity class won’t be seen by the automatic Fluent API code.

  • The configuration commands must apply the same configurations every time because the EF Core configures the application’s DbContext only once—on first use—and then works from a cache version.

Summary

  • The first time you create the application’s DbContext, EF Core configures itself by using a combination of three approaches: By Convention, Data Annotations, and the Fluent API.

  • Value converters allow you to transform the software type/value when writing and reading back from the database.

  • Two EF Core features, shadow properties and backing fields, allow you to hide data from higher levels of your code and/or control access to data in an entity class. Use the By Convention approach to set up as much as you can, because it’s simple and quick to code.

  • When the By Convention approach doesn’t fit your needs, Data Annotations and/or EF Core’s Fluent API can provide extra commands to configure both the way EF Core maps the entity classes to the database and the way EF Core will handle that data.

  • In addition to writing configuration code manually, you can also add code to configure entity classes and/or properties automatically based on the class/ properties signature.

For readers who are familiar with EF6:

  • The basic process of configuring EF Core is, on the surface, similar to the way EF6 works, but there is a significant number of changed or new commands.

  • EF Core can use configuration classes to hold the Fluent API commands for a given entity class. The Fluent API commands provide a feature similar to the EF6.x EntityTypeConfiguration<T> class, but EF Core uses an IEntityTypeConfiguration<T> interface instead.

  • EF Core has introduced many extra features that are not available in EF6, such as value converters, shadow properties, and backing fields, all of which are welcome additions to EF.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.200.180