- Use public getters and private setters for the properties. C# creates the corresponding backing fields implicitly.
- EF Core, by default, binds to properties backing fields. Uses reflection to find those backing fields and assigns values to them.
- C# creates parameterless public constructor by default if no other ctor is defined. Always define one with the required arguments.
- Make sure the function argument names are the
lowerCamelCase
versions of the class property names.
- Make sure the function argument names are the
Based on my research, the value objects can be configured in two ways.
Let's define the value objects first:
using System;
using Bcan.Backend.SharedKernel;
// single property value object
public class Email : ValueObject
{
public Email(string value)
{
if(string.IsNullOrWhiteSpace(value))
throw new ArgumentNullException(nameof(value));
// Can do additional checks on format
Value = value;
}
public string Value { get; private set; }
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Value;
}
}
// Multi-property value object
public class Address : ValueObject
{
public Address(string street, string city)
{
if(string.IsNullOrWhiteSpace(street))
throw new ArgumentException(nameof(street));
if(string.IsNullOrWhiteSpace(city))
throw new ArgumentException(nameof(city));
// Additional validations can be done
Street = street;
City = city;
}
public string Street { get; private set; }
public string City { get; private set; }
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Street;
yield return City;
}
public Address NewWithSameStreet(string city)
{
return new Address(this.Street, city);
}
public Address NewWithSameCity(string street)
{
return new Address(street, this.City);
}
}
Now define the aggregates:
using System;
using Bcan.Backend.SharedKernel;
public sealed class Instructor : BaseEntity<Guid>
{
// This private ctor is required for option 1 (owned entity)
// Otherwise it throws InvalidOperationException for ctor not found.
private Instructor() : base(Guid.NewGuid()) {}
public Instructor(Email email, Address address) : base (Guid.NewGuid())
{
if(email is null)
throw new ArgumentNullException(nameof(email));
if(address is null)
throw new ArgumentNullException(nameof(address));
// Additional validation can be done here
Email = email;
Address = address;
}
public Email Email { get; private set; }
public Address Address { get; private set; }
}
- Option 1 - As Owned Entity:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Do not let the id to be generated by the db
modelBuilder.Entity<Instructor>(i => {
i.Property(ip=>ip.Id).ValueGeneratedNever();
});
modelBuilder.Entity<Instructor>()
.OwnsOne(i=>i.Email); // Column_name: Email_Value
// For custom coolumn name, uncomment the following
/*
modelBuilder.Entity<Instructor>()
.OwnsOne(i=>i.Email, ie =>
{
ie.Property(iep=>iep.Value).HasColumnName("Email");
});
*/
modelBuilder.Entity<Instructor>()
.OwnsOne(i=>i.Address); // Address_Street and Address_City column names
}
Owned Entities has some limitations by design which avoids users to create DbSet and call Entity. Since it is used on value objects, having this limitation does not affect our design in DDD context.
- Option 2 - Using Value Conversion
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Disable value objects to avoid generating separate tables for them
modelBuilder.Ignore<Email>();
modelBuilder.Ignore<Address>();
// Do not let the id to be generated by the db
modelBuilder.Entity<Instructor>(i => {
i.Property(ip=>ip.Id).ValueGeneratedNever();
});
// Single Property Value Objects
var emailValueConverter = new ValueConverter<Email, string>
(
v => v.Value, // to string
v => new Email(v) // from string
);
modelBuilder.Entity<Instructor>()
.Property(i=>i.Email)
.HasConversion(emailValueConverter);
// Multiple property value objects
// Since it is not allowed to map (convert) a property into multiple columns
// the properties of the value objects can be serialized to/ deserialized from a single column.
// i.e. Json using System.Text.Json
modelBuilder.Entity<Instructor>()
.Property(i => i.Address)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<Address>(v, (JsonSerializerOptions)null));
}
Value converters can only be used to convert a property into a single column. EF Core 6.0 might allow conversion of a property into multiple columns.