Search code examples
c#entity-frameworkdomain-driven-designrepository-patternonion-architecture

Setting the identity of a Domain Entity


All entities in the domain need to have identity. By inheriting from DomainEntity, I am able to provide identity to classes.

City domain entity (stripped down for easy reading):

public class City : DomainEntity, IAggregateRoot
{
    public string Name { get; private set; }

    public Coordinate Coordinate { get; private set; }

    public City(string name, decimal latitude, decimal longitude) 
    {
        Name = name;
        SetLocation(latitude, longitude);
    }

    public City(string name, decimal latitude, decimal longitude, int id) 
        : base(id)
    {
        Name = name;
        Coordinate = coordinate;
        SetLocation(latitude, longitude);
    }

    public void SetLocation(decimal latitude, decimal longitude)
    {
        Coordinate = new Coordinate(latitude, longitude);
    }
}

DomainEntity abstract class:

public abstract class DomainEntity
{
    private int? uniqueId;

    public int Id
    {
        get
        {
            return uniqueId.Value;
        }
    }

    public DomainEntity()
    { }

    public DomainEntity(int id)
    {
        uniqueId = id;
    }
}

When a new entity is first created, an identity does not exist. Identity will only exist once the entity is persisted. Because of this, when creating a new instance of the entity, Id does not need to be supplied:

var city = new City("Cape Town", 18.42, -33.92);

When cities are read from persistence using a CityRepository, then the second constructor will be used so to populate the identity property as well:

public class CityRepository : ICityRepository
{
    public City Find(int id)
    {
        var cityTblEntity = context.Set<CityTbl>().Find(id);

        return new City(cityTblEntity.Name, cityTblEntity.Lat, cityTblEntity.Long, cityTblEntity.Id);
    }
}

The problem I am having here is that I provide a constructor which can take in identity. This opens up a hole. I only want identity to be set in the repository layer, but client code could now also start setting Id values. What's stopping someone from doing this:

var city = new City("Cape Town", 18.42, -33.92, 99999);  // What is 99999? It could even be an existing entity!

How can I provide ways to set entity identity in my repository but to hide that from client code? Perhaps my design is flawed. Could I use factories to solve this?

Note: I understand that this is not a perfect implementation of DDD as entities should have identity from the beginning. The Guid type would help me solve this problem, but I don't have that luxury unfortunately.


Solution

  • In addition to Ilya Palkin's answer I want to post another solution which is simpler but a bit tricky:

    1. Make DomainEntity.UniqueId protected, so it can be accessed from its childs
    2. Introduce a factory (or static factory method) and define it inside City class, so it can access the DomainEntity.UniqueId protected field.

    Pros: No reflection, code is testable.
    Cons: Domain layer knows about DAL layer. A little bit tricky definition of the factory.

    The code:

    public abstract class DomainEntity
    {
        // Set UniqueId to protected, so you can access it from childs
        protected int? UniqueId;
    }
    
    public class City : DomainEntity
    {
        public string Name { get; private set; }
    
        public City(string name)
        {
            Name = name;
        }
    
        // Introduce a factory that creates a domain entity from a table entity
        // make it internal, so you can access only from defined assemblies 
        // also if you don't like static you can introduce a factory class here
        // just put it inside City class definition
        internal static City CreateFrom(CityTbl cityTbl)
        {
            var city = new City(cityTbl.Name); // or use auto mapping
            // set the id field here
            city.UniqueId = cityTbl.Id;
            return city;
        }
    }
    
    public class CityTbl
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
    
    static void Main()
    {
        var city = new City("Minsk");
    
        // can't access UniqueId and factory from a different assembly
        // city.UniqueId = 1;
        // City.CreateFrom(new CityTbl());
    }
    
    // Your repository will look like
    // and it won't know about how to create a domain entity which is good in terms of SRP
    // You can inject the factory through constructor if you don't like statics
    // just put it inside City class
    public class CityRepository : ICityRepository
    {
        public City Find(int id)
        {
            var cityTblEntity = context.Set<CityTbl>().Find(id);
    
            return City.CreateFrom(cityTblEntity);
        }
    }