Setting the identity of a Domain Entity

十年热恋 提交于 2019-12-03 20:43:01

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

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

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.

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