Setting the identity of a Domain Entity

我们两清 提交于 2019-12-09 13:42:24

问题


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.


回答1:


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



回答2:


My feeling is that a null id satisfies identity--i.e., this is a new or potential entity. I would use a single constructor as follows:

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



回答3:


I see the following options:

  1. Static method on Entity type that has access to private fields.

    public class Entity
    {
        private int? id;            
        /* ... */           
        public static void SetId(Entity entity, int id)
        {
            entity.id = id;
        }
    }
    

    Usage:

        var target = new Entity();
        Entity.SetId(target, 100500);
    
  2. Reflection can be used to get access to private field

    public static class EntityHelper
    {
        public static TEntity WithId<TEntity>(this TEntity entity, int id)
            where TEntity : Entity
        {
            SetId(entity, id);
            return entity;
        }
    
        private static void SetId<TEntity>(TEntity entity, int id)
            where TEntity : Entity
        {
            var idProperty = GetField(entity.GetType(), "id", BindingFlags.NonPublic | BindingFlags.Instance);
            /* ... */   
            idProperty.SetValue(entity, id);
        }
    
        public static FieldInfo GetField(Type type, string fieldName, BindingFlags bindibgAttr)
        {
            return type != null
                ? (type.GetField(fieldName, bindibgAttr) ?? GetField(type.BaseType, fieldName, bindibgAttr))
                : null;
        }
    }
    

    Usege:

        var target = new Entity().WithId(100500);
    

    Full code is available as a gist on GitHub.

  3. Automapper can be used since it uses reflection and can map private properties.

    I checked it answering How to retrieve Domain Object from Repositories

    [TestClass]
    public class AutomapperTest
    {
        [TestMethod]
        public void Test()
        {
            // arrange
            Mapper.CreateMap<AModel, A>();
            var model = new AModel { Value = 100 };
    
            //act
            var entity = Mapper.Map<A>(model);
    
            // assert
            entity.Value.Should().Be(100);
            entity.Value.Should().Be(model.Value);
        }
    }
    
    public class AModel
    {
        public int Value { get; set; }
    }
    
    public class A
    {
        public int Value { get; private set; }
    } 
    

PS: An implementation of DomainEntity.Id might lead to InvalidOperationException when uniqueId is not set.

EDIT:

But, in this case, wouldn't factory methods just provide a thin veneer over each constructor? I have always known factories to be used to create complex instances atomically so that the domain 'rules' are not violated, perhaps entities with associations and aggregations.

These factory methods can be used in order to create new instances in your system. There is an advantage that it is possible to give them any name with clear description. Unfortunately it is difficult to mock them since they are static.

If testability is the goal then separate factories can be developed.

Factories have several benefits over constructors:

  • Factories can tell about the objects they are creating
  • Factories are polymorphic, in that, they can return the object or any subtype of the object being created
  • In cases where the object being created has a lot of optional parameters, we can have a Builder object as the Factory

It would be great if I could use factories to create new instances with identity, but wouldn't those factories still need to call on those public identity-burdened constructors?

I think that something public is needed anyway unless you use reflection.

There might be another solution. Instead of public constructor your entities can apply some kink of commnd or 'specifications' with id value in it.

    public void Apply(AppointmentCreatedFact fact)
    {
        Id = fact.Id;
        DateOfAppointment = fact.DateOfAppointment;
    }

But I preffer static method on 'Entity' type since it is not so obvious to invoke.

I do not think that public constructor is an evil. It is an evil when the constructor is invoked in lots of places and adding a new parameter into it results in endless compilation errors fixing. I suggest you control places where constructors of your domain entities are invoked.




回答4:


The most simple solution is the make all constructors with Id as internal which requires minimum changes.

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

    // Just make it internal
    internal City(string name, decimal latitude, decimal longitude, int id)
        : base(id)
    {
        Name = name;
        Coordinate = coordinate;
        SetLocation(latitude, longitude);
    }
}


来源:https://stackoverflow.com/questions/21452984/setting-the-identity-of-a-domain-entity

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!