Skip to content

Latest commit

 

History

History
173 lines (138 loc) · 5.5 KB

DDD_EFCore.md

File metadata and controls

173 lines (138 loc) · 5.5 KB

DDD and EF Core

Domain classes

  • 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.
    1. Make sure the function argument names are the lowerCamelCase versions of the class property names.

Value Objects

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; }


}
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.

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.

References