Efcore implementation value object of DDD
Region
public record Region
{
public long Id { get; init; }
public MultilingualString Name { get; init; }
public Area Area { get; init; }
public RegionLevel Level { get; private set; }
public long? Population { get; private set; } // population
public Geo Location { get; init; }
private Region() { }
public Region(MultilingualString name, Area area, Geo location,
RegionLevel level)
{
this.Name = name;
this.Area = area;
this.Location = location;
this.Level = level;
}
public void ChangePopulation(long value)
{
this.Population = value;
}
public void ChangeLevel(RegionLevel value)
{
this.Level = value;
}
}
//Multilingual
public record MultilingualString(string Chinese, string? English);
//Area size
public record Area(double Value, AreaType Unit);
public enum AreaType
{
SquareKm, // m2
Hectare, // ha
CnMu //
}
//City level
public enum RegionLevel
{
Province, // Province
City, // City
Country, // Country
Town // town
}
//Longitude and latitude
public record Geo
{
public double Longitude { get; init; }
public double Latitude { get; init; }
public Geo(double longitude, double latitude)
{
if (longitude < -180 || longitude > 180)
{
throw new ArgumentException("longitude invalid");
}
if (latitude < -90 || latitude > 90)
{
throw new ArgumentException("longitude invalid");
}
this.Longitude = longitude;
this.Latitude = latitude;
}
}
RegionConfig
class RegionConfig : IEntityTypeConfiguration
{
public void Configure(EntityTypeBuilder builder)
{
builder.ToTable("T_Cities");
builder.OwnsOne(c => c.Area, nb =>
{
nb.Property(e => e.Value)
.HasColumnName("AreaValue")
. hascomment ("area size");
nb.Property(e => e.Unit)
.HasColumnName("AreaUnit")
.HasMaxLength(20).IsUnicode(false)
.hasconversion() Hascomment ("unit (m2 / HA)");
});
builder. Property(e => e.Population). Hascomment ("population");
builder.OwnsOne(c => c.Location, nb =>
{
nb.Property(e => e.Longitude)
.HasColumnName("Longitude")
. hascomment ("longitude");
nb.Property(e => e.Latitude)
.HasColumnName("Latitude")
. hascomment ("dimension");
});
builder.Property(c => c.Level).HasMaxLength(20)
.IsUnicode(false).HasConversion()
. hascomment ("city level (country / Province / city / town)");
builder.OwnsOne(c => c.Name, nb =>
{
nb.Property(e => e.English).HasMaxLength(20).IsUnicode(false)
.HasColumnName("NameEnglish")
. hascomment ("English");
nb.Property(e => e.Chinese).HasMaxLength(20).IsUnicode(true)
.HasColumnName("NameChinese")
. hascomment ("Chinese");
});
}
}
(1) “Owned entities”: configured using methods such as ownsone in fluent API.
(2) In EF core, the attribute of an entity can be defined as an enumeration type. By default, the attribute of an enumeration type is saved as an integer type in the database. In EF core, hasconversion can be used in fluent API() configure the value of enumeration type to be saved as string.
(4) View database
Execute the commands add mirgration and update database
(5) Simplify the comparison of value objects
ExpressionHelper
class ExpressionHelper
{
public static Expression> MakeEqual
(Expression> propAccessor, TProp? other)
where TItem : class where TProp : class
{
var e1 = propAccessor.Parameters.Single();
BinaryExpression? conditionalExpr = null;
foreach (var prop in typeof(TProp).GetProperties())
{
BinaryExpression equalExpr;
object? otherValue = null;
if (other != null)
{
otherValue = prop.GetValue(other);
}
Type propType = prop.PropertyType;
var leftExpr = MakeMemberAccess(propAccessor.Body, prop);
Expression rightExpr = Convert(Constant(otherValue), propType);
if (propType.IsPrimitive)
{
equalExpr = Equal(leftExpr, rightExpr);
}
else
{
equalExpr = MakeBinary(ExpressionType.Equal,
leftExpr, rightExpr, false,
prop.PropertyType.GetMethod("op_Equality")
);
}
if (conditionalExpr == null)
{
conditionalExpr = equalExpr;
}
else
{
conditionalExpr = AndAlso(conditionalExpr, equalExpr);
}
}
if (conditionalExpr == null)
{
throw new ArgumentException("There should be at least one property.");
}
return Lambda>(conditionalExpr, e1);
}
}