Entity Framework Core deals with object values with Value Converters and Value Comparers. The Value Converters, as the name suggest, helps in converting object values from one type to another while Value Comparers helps in comparing the object values needed for change tracking, custom comparision codes, etc.
Download the source code from GitHub.
Page Contents
Lets take an example of differnt products in an ecommerce site. Each product can be given multiple tags like – a trouser can have tags “men trousers, men pants, formal trouser, white formal trouser”, similarly a Mobile can have tags “mobile, cell phone, iphone, 5g phone, china mobile”.
We defined a Product.cs class which is given below. Note the Tags property which will store values as “men trousers, men pants, formal trouser, white formal trouser” on the Tags column in the database.
public class Products
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<string> Tags { get; set; }
}
Next, we configure the Value Converters and Value Comparers in the OnModelCreating method of the database context. See the below code.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>()
.Property(e => e.Tags)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<List<string>>(v, (JsonSerializerOptions)null),
new ValueComparer<ICollection<string>>(
(c1, c2) => c1.SequenceEqual(c2),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => (ICollection<string>)c.ToList()));
}
We used the HasConversion method to tell EF Core that for the property Tags perform 2 things.
First is to serialize the value of “Tags” property to json as it goes into the database. This is done by the code line.
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null)
Secondly when reading it’s value from the database it should deserialize it to List<string> so that the Tags property can hold the data. The code line which performs this work is given below.
v => JsonSerializer.Deserialize<List<string>>(v, (JsonSerializerOptions)null)
The value comparer code is needed by Entity Framework Core Change tracking mechanism to find out if the value of the tags is changed so that the changes can be stored on the database. The code line for Value Comparer is given below.
new ValueComparer<ICollection<string>>(
(c1, c2) => c1.SequenceEqual(c2),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => (ICollection<string>)c.ToList()));
See the above ValueComparer method which accepts three expressions:
The above example can be added to the Product Create and Update feature in the app. See the below image where during the time of creation of a product called “Trouser”, we are adding 2 tags to it, these are “men trousers”,”men pants”.
Next, we can also update the Product and add 2 more tags (“formal trouser”,”white formal trouser”) to it.
The tags are stored in json format in the database.
["men trousers","men pants","formal trouser","white formal trouser"]
The below imags shows the snapshot of the database, check the tags json.
You can download the full source codes of the Create and Update feature from my GitHub repository. The link is given at the start of this tutorial.
With Value Converters we can perform conversions from enum values to strings in the database and from strings to enums while reading. Take for example we have used an enum called “MarketingType” to represent different types of Marketings which are “Email, SMS, Digital, SearchEngine”.
public class Marketing
{
public int Id { get; set; }
public MarketingType Type { get; set; }
public string Comment { get; set; }
}
public enum MarketingType
{
Email,
SMS,
Digital,
SearchEngine
}
Next, in the OnModelCreating method we configured the conversions as shown below.
modelBuilder.Entity<Marketing>()
.Property(e => e.Type)
.HasConversion(
v => v.ToString(),
v => (MarketingType)Enum.Parse(typeof(MarketingType), v));
Conversions are defined using 2 Func expression trees. The first one from ModelClrType to ProviderClrType. This is basically for saving the values to the database.
The other from ProviderClrType to ModelClrType. This does the conversion of the values read from the database to the corresponding entities holding these values.
In the HasConversion method there are 2 parameters provided in Func expression type. First one for converting from the enum to the string for saving in the database, and another for the opposite conversion i.e from string back to enum.
The above same configuration can also be performed by using the generic version of HasConversion() method as shown below.
modelBuilder.Entity<Marketing>()
.Property(e => e.Type)
.HasConversion<string>();
The ValueConverter class ValueConverter<TModel,TProvider> class is defined using 2 Func expression trees – ModelClrType and ProviderClrType. The HasConversion() method creates an instance of ValueConverter class to configure the conversions.
We can also use the ValueConverter class to create converters. See the below code which is basically doing the same thing for converting enum to string and vice versa.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var converter = new ValueConverter<MarketingType, string>(v => v.ToString(), v => (MarketingType)Enum.Parse(typeof(MarketingType), v));
modelBuilder.Entity<Marketing>()
.Property(e => e.Type)
.HasConversion(converter);
}
If there are multiple properties in differnt entities and these properties need common conversions then we can simply provide conversions in bulk manner and not do it manually for each property.
In the below example we are converting each string property to Uppercase before saving in the database. We defined ToUpperConverter.cs class as shown below.
public class ToUpperConverter : ValueConverter<string, string>
{
public ToUpperConverter() : base(
v => v.Trim().ToUpper(),
v => v)
{
}
}
Then inside OnModelCreating method we provide the following configuration.
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder
.Properties<string>()
.HaveConversion<ToUpperConverter>();
}
Entity Framework Core has a number of built-in value converters that can reduce our work from writing converters manually. Let us know some of the most important ones.
Kindly note that EF Core has built-in logic snapshotting and comparing the standard types that are used in databases, therefore we don’t have to configure Value Comparers for them. But for types that are complex, we need to configure value Comparers. In the below examples of built-in converters, we don’t have to configure Value Comparers since EF Core will automatically do this job for us.
For converting bool values (true, false) to Numeric (0, 1), we can use HasConversion<int>(). Example – A Student entity has PassStatus property of bool type. This property is meant for storing whether the student has passed the exam or not. It can have values either true or false. We can perform value conversion on this to store it in the database as numerical 1 or 0 instead of true or false. See the below code:
public class Student
{
public int Id { get; set; }
public bool PassStatus { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>()
.Property(e => e.PassStatus)
.HasConversion<int>();
}
In the view we can add select tag to contain boolean true/false values for the PassStatus property.
<select asp-for="PassStatus" class="form-control">
<option value="true">True</option>
<option value="false">False</option>
</select>
Note: During migration EF Core will create PassStatus column as int type instead of ususal bit type column for storing bool values.
We can also configure the converters for bool to string i.e. true/false to “1”, “0”. This can be done by HasConversion<string>().
public class Student
{
public int Id { get; set; }
public bool PassStatus { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>()
.Property(e => e.PassStatus)
.HasConversion<string>();
}
In this case the PassStatus column in the database will be created as NVARCHAR (1)
.
For converting strings to types bool, char, DateTime & Guid, we can use the same HasConversion method.
public class Student
{
public int Id { get; set; }
public string PassStatus { get; set; }
}
// convert string to bool
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>()
.Property(e => e.PassStatus)
.HasConversion<bool>();
}
// convert string to char
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>()
.Property(e => e.PassStatus)
.HasConversion<char>();
}
// convert string to DateTime
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>()
.Property(e => e.PassStatus)
.HasConversion<DateTime>();
}
// convert string to Guid
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>()
.Property(e => e.PassStatus)
.HasConversion<Guid>();
}
In the below example we configured the converter for char to string.
public class Student
{
public int Id { get; set; }
public char PassStatus { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>()
.Property(e => e.PassStatus)
.HasConversion<String>();
}
In the below example we configured the DateTime to long.
public class Student
{
public int Id { get; set; }
public char PassStatus { get; set; }
public DateTime JoiningDate { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>()
.Property(e => e.JoiningDate)
.HasConversion<long>();
}
Instead of having separate database columns for entity properties we can have a single database column for multiple entity properties. This is known as composite objects. Let’s understand this with an example.
We have an Order.cs class having 2 properties Id and Price. The Price property is a composite property since it is of another class type, here Money.cs. The Price.cs class is:
public class Order
{
public int Id { get; set; }
public Money Price { get; set; }
}
The Money class is defined below.
public class Money
{
public Money()
{
}
[JsonConstructor]
public Money(decimal amount, Currency currency)
{
Amount = amount;
Currency = currency;
}
public override string ToString()
=> (Currency == Currency.UsDollars ? "$" : "£") + Amount;
public decimal Amount { get; set; }
public Currency Currency { get; set; }
}
public enum Currency
{
UsDollars,
PoundsSterling
}
The Money class contains both the amount and currency. The Amount is of decimal type and Currency is an enum.
We want to make Price column in the database table to store both the Amount and Currency values in JSON format. So we configured the value converter for it inside the OnModelCreating method as shown below.
modelBuilder.Entity<Order>()
.Property(a => a.Price)
.HasConversion(
b => JsonSerializer.Serialize(b, (JsonSerializerOptions)null),
b => JsonSerializer.Deserialize<Money>(b, (JsonSerializerOptions)null));
The EF Core will create the Price column as [Price] NVARCHAR (MAX) type in the database and it will store the JSON, something like {"Amount":103,"Currency":1}
.
We can read the JSON value in different HTML tags in the UI as shown in the below image.
Note that in this example EF Core can snapshot and compare values without having to need Value Comparers.
You will find this example in the source codes.
SQL Server uses 8-byte array for performing optimistic concurrency. We can change it to ulong type and with value converters map the rowversion to this ulong property. See the example below.
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public ulong Version { get; set; }
}
modelBuilder.Entity<Product>()
.Property(e => e.Version)
.IsRowVersion()
.HasConversion<byte[]>();
With Value Converters we can encrypt property values before sending them to the database, and decrypt them when reading them from the database.
In the below example we are encrypting and decrypting the password property of bank accounts.
modelBuilder.Entity<Bank>().Property(e => e.Password).HasConversion(
v => new string(v.Reverse().ToArray()),
v => new string(v.Reverse().ToArray()));
Enity Framework Core is smart enough and does the comparing of standard types used in databases by it’s own. By default the Value types (int, float, struct) are copied to produce the snapshot, while the same instance is used for the reference types (class, list) and no copying occurs. Remember that when a complex property is mapped through a value converter, only then you have to write the Value Comparer.
There are 2 ways to perform value comparing – 1) Shallow Comparision 2) Deep Comparision./p>
In Shallow Comparision EF Core uses the equality comparison which is Equals method for snapshotting.
The below example shows Value Compasion for a struct done in Shallow manner.
public class ExampleEntity
{
public int Id { get; set; }
public ExampleEntity Property1 { get; set; }
}
public readonly struct ExampleStruct
{
public ExampleStruct(int value)
{
Value = value;
}
public int Value { get; }
}
The configuration in the OnModelCreating method is given below.
modelBuilder
.Entity<ExampleEntity>()
.Property(e => e.Property1)
.HasConversion(
v => v.Value,
v => new ExampleStruct(v));
Note that EF Core will do it automatically so you don’t have to write the above configruation in OnModelCreating method.
In Deep Comparision, EF Core uses SequenceEqual method to check if two sequences are equal. It also uses HashCode method to calculate the hashcode used for performing the equality testing of 2 values.
The below example shows the Value Comparer done for a Immutable class i.e. Sealed.
In this class we have overridden methods – Equals and GetHashCode, for testing the equality. Note that it is not necessary to have these two methods since EF Core will use it’s default mechanism for checking equality. Ony if we want to write our own cusom checking code, then only we should have these 2 methods.
public class SomeEntity
{
public int Id { get; set; }
public TestImmutableClass Property1 { get; set; }
}
public sealed class TestImmutableClass
{
public TestImmutableClass(int value)
{
Value = value;
}
public int Value { get; }
private bool Equals(TestImmutableClass other)
=> Value == other.Value;
public override bool Equals(object obj)
=> ReferenceEquals(this, obj) || obj is TestImmutableClass other && Equals(other);
public override int GetHashCode()
=> Value.GetHashCode();
}
The configuration in the OnModelCreating method is given below.
modelBuilder
.Entity<SomeEntity>()
.Property(e => e.Property1)
.HasConversion(
v => v.Value,
v => new TestImmutableClass(v));
Note that EF Core will do it automatically so you don’t have to write the above configruation in OnModelCreating method.
Where the EF Core default behavior is not suitable, you may provide a value comparer, which contains logic for snapshotting, comparing and calculating a hash code.
In the below example we are adding our custum value comparer.
public class Products
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<string> Tags { get; set; }
}
See the below code where we added our custom value comparer.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>()
.Property(e => e.Tags)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<List<string>>(v, (JsonSerializerOptions)null),
new ValueComparer<ICollection<string>>(
(c1, c2) => c1.SequenceEqual(c2),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => (ICollection<string>)c.ToList()));
}
In the above code the Tags property is a reference type. To compare it we did 3 things:
Entity Framework default value comparison does not work in certain cases so we need to override it.
In case of byte array, when the byte array property is non-key, Entity Framework Core will use Shallow Comparison. This is it will make a copy for comparison.
While for byte arrays defined as keys, EF Core will use Deep Comparison. So the same instance is used for the reference types and no copying occurs. This will fail since it is unlikely that an foreign key property is set to the same instance as a primary key property to which it needs to be compared.
In the below example we tell EF Core to compare byte sequences and will therefore detect byte array mutations.
public class TestEntity
{
public int Id { get; set; }
public byte[] MyBytes { get; set; }
}
modelBuilder
.Entity<TestType>()
.Property(e => e.MyBytes)
.Metadata
.SetValueComparer(
new ValueComparer<byte[]>(
(c1, c2) => c1.SequenceEqual(c2),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => c.ToArray()));
In this tutorial we covered in details how Value Converters and Value Comparers work in Entity Framework Core. We understood their workings by seeing some of the freqently come across problems and their solutions. We also understand how Deep and Shallow Comparison works and where we will need Value Comparers in our codes. Dont forget to download the GitHub repository for the source codes. Link is at the beginning of this tutorial.