EF Core has powerful options to map your domain model to a relational database. In this article, I’ll show you how to use the Fluent API to configure a hierarchy of generic classes to map to a single table, and to convert a collection to a store a single string in the database.
The Model
The model that’s defined defines the types GameData
, and MoveData
. The GameData
class specifies a few simple properties as well as a property of type ICollection
that specifies a relationship. Using the primary constructor syntax, the C# compiler creates init-only properties and a constructor that initializes the properties. EF Core doesn’t require parameterless constructors. However, relations cannot be assigned using the constructor. The Moves
property of type ICollection
is initialized with a List
. In this scneario, the collection contains types deriving from the MoveData
class.
With this sample, the ‘MoveData’ type is of special interest. MoveData
is an abstract base class. The generic version MoveData
is derived from MoveData
. The MoveData
class specifies a property of type ICollection
that has a collection of fields of a single game move. The type TField
is a generic parameter which allows creating different game types. For example, a move can consist of a list of different colors, or with another more complex game type, a list of different shapes and colors. The values of the MoveData
class as well as the generic classes deriving from it should be stored in a single table. The field values specified as a collection property should be stored in the same table as a single string.
The types that will be used for the generic type parameter TField
are the records ColorField
and ShapeAndColorField
. ColorField
wraps a color specified by a string, ShapeAndColorField
wraps two strings specifying a shape and a color. These types will be discussed later in more detail.
EF Core Fluent API
Specifying the mapping of the GameData
type is done using the Fluent API. The table the class is mapped to is Games
, the primary key and column properties are specified as shown in the following code snippet.
Specifying a One-to-Many Relation
The relation between GameData
and MoveData
is specified with the following code snippet. You can specify a one-to-many relation using the HasOne
and WithMany
methods. Using DeleteBehavior.Cascade
specifies that if a game is deleted, the game moves will be deleted as well. There’s a one-to-many relation specified these two types, and a cascade delete. The a game is deleted, all its moves are deleted as well.
Using Table per Hierarchy Mapping Strategy
Using the Table per Hierarchy (TpH) TpH hierarchy mapping it’s possible to map a hierarchy of types to a single table. Using the HasDiscriminator
method, the column can be specified that will be used as a differentiator column to give EF Core the information which types should be created on reading the data. The HasDiscriminator
method is used together with HasValue
. HasDiscriminator
returns a DiscriminatorBuilder
. HasValue
is a method of this builder that is used to specify the value that should be stored within the discriminator column when an instance of the specified type is stored. With the sample code, the value color
is stored with a MoveData
instance, and the value shape
is stored with a MoveData
instance.
> EF Core also supports the Table per Type (TpT) and Table per Concrete Type (TpC) hierarchies. With TpT for every type a different table is created. With TpC for all concrete types a single table is mapped. Thus, abstract types are not mapped to a table.
Converting Values
EF Core supports custom conversions from a .NET type to a column since EF Core 5.0 with the method HasConversion
. Using this method, two delegates can be assigned to convert the .NET type to a column, and the other way around.
Before specifying the conversion, the types need to be convertable. The types that should be stored inside a single string are the ColorField
and the ShapeAndColorField
records. To convert them to a string, the ToString method is overridden:
To create a ColorField
or ShapeAndColorField
instance from a string, the static methods Parse
and TryParse
can be used. Some types from .NET support these methods, for example the Int32 type to convert a string to a number. One of the new C# language features with C# 11 is the ability to define static members in interfaces. This allows using operators or other static members in generic classes. The IParsable
interface defines the Parse
and TryParse
methods – as a static member.
The ColorField
and ShapeAndColorField
types implement this interface. The ColorField
and ShapeAndColorField
types can be converted to a string, and a string can be converted to a ColorField
or ShapeAndColorField
instance.
> Maybe you’re wondering about the NotNullWhen
and MaybeNullWhen
attributes. These attributes give hints to the compiler about nullable reference types behavior. These attributes act on the return type of the TryParse
method. Using NotNullWhen(true)
gives the compiler the informatation if the TryParse
method returns true, the argument annotated with NotNullWhen(true)
is not null after the method returns. This means that if the variable that’s assigned to the variable s is not null after the method returns with a true
value, and the compiler should not complain to require null checks. The TryParse
method returns the last paramter with the out
modifier. Here, the MaybeNullWhen(false)
annotation is applied. If the TryParse
method returns false
, the compiler should complain about using the out value without a null check. If TryParse
fails, the sample implementation returns null
. The out type is not declared nullable, so the compiler does not complain about null checks if the TryParse
method does not return false
.
With the sample code, conversion to and from a string with the ColorField
and the ShapeAndColorField
is not the only requirement. The MoveData
classes uses a collection of these types that needs to be converted. To do this, the MappingExtensions
class defines the extension methods ToFieldString
and ToFieldCollection
.
The ToFieldString
method invokes the ToString
method with every item in the collection and joins the results with a colon. The ToFieldCollection
method splits the string on the colon and invokes the Parse
method on every item. Because the Parse
method is now specified with the IParsable
interface, the generic type T
can now be used to invoke this method.
Now all what’s needed is to specify the conversion. The next code snippet shows the conversion for the Fields
property of the MoveData
class to invoke the ToFieldString
method using the ICollection
fields
parameter, ant the ToFieldCollection
method using the string
fields
parameter.
> Instead of converting the ColorField
and ShapeAndColorField
types to a string, and back, the Fields
property can be mapped to a JSON column. How ths feature can be used will be shown in a future article.
Take away
EF Core offers a powerful mapping from the object model to relational databases. It’s not only possible to use constructors with arguments, but also map inherited classes to tables – as shown with the table per hierarchy mapping. The sample code uses generic classes and an abstract base class which all are mapped to the same table. Using value conversions, it’s possible to map complex types to a single value. Value conversion is also a great way to map enum values to a representation in the database.
With this article I’ve also shown a great new C# 11 feature – the ability to define static members in interfaces. The IParsable
interface is used in a generic extension method to convert a string to a collection of a generic type by parsing a string to create instances of the generic type.
Enjoy learning and programming!
Christian
If you enjoyed this article, I’m thankful if you support me with a coffee. Thanks!

More Information
More information about programming EF Core is available in my book and my workshops.
Read more about EF Core and mapping objects to relations in my book Professional C# and .NET – 2021 Edition
See Chapter 21, "Entity Framework Core".
The complete source code of this sample is available on Professional C# source code – see the folder 5_More/EFCore/InhertianceMappingWithConversion
4 thoughts on “EF Core Mapping with TpH, Generic Types and Value Conversion”