设置域实体的标识

时间:2014-01-30 09:51:33

标签: c# entity-framework domain-driven-design repository-pattern onion-architecture

域中的所有实体都需要具有身份。通过继承DomainEntity,我能够为类提供身份。

城市域名实体(为了便于阅读而被删除):

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抽象类

public abstract class DomainEntity
{
    private int? uniqueId;

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

    public DomainEntity()
    { }

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

首次创建新实体时,不存在标识。只有在实体持久化后,才会存在身份。因此,在创建实体的新实例时,不需要提供Id

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

当使用CityRepository从持久性中读取城市时,将使用第二个构造函数来填充身份属性:

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

我在这里遇到的问题是我提供了一个可以接受身份的构造函数。这打开了一个洞。我只想在存储库层中设置标识,但客户端代码现在也可以开始设置Id值。什么阻止某人这样做:

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

我如何提供在我的存储库中设置实体标识但是从客户端代码中隐藏它的方法?也许我的设计存在缺陷。我可以用工厂来解决这个问题吗?

注意:我知道这不是DDD的完美实现,因为实体应该从头开始具有身份。 Guid类型可以帮助我解决这个问题,但遗憾的是我没有这种奢侈品。

4 个答案:

答案 0 :(得分:4)

除了Ilya Palkin的回答,我想发布另一个更简单但有点棘手的解决方案:

  1. 使DomainEntity.UniqueId受到保护,因此可以从其子级访问
  2. 介绍工厂(或静态工厂方法)并在City类中定义它,以便它可以访问DomainEntity.UniqueId受保护字段。
  3. 优点:没有反思,代码是可测试的。
    缺点:域层知道DAL层。对工厂的定义有点棘手。

    代码:

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

答案 1 :(得分:0)

我看到以下选项:

  1. Entity类型上的静态方法,可以访问私有字段。

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

    用法:

        var target = new Entity();
        Entity.SetId(target, 100500);
    
  2. 可以使用反射来访问私有字段

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

    完整代码以gist on GitHub

  3. 提供
  4. Automapper可以使用,因为它使用反射并可以映射私有属性。

    我查了回答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; }
    } 
    
  5. PS: 如果未设置DomainEntity.IdInvalidOperationException的实施可能会导致uniqueId

    修改

      

    但是,在这种情况下,工厂方法不会只为每个构造函数提供一个薄的贴面吗?我一直都知道工厂用于以原子方式创建复杂实例,以便不违反域“规则”,可能是具有关联和聚合的实体。

    可以使用这些工厂方法在系统中创建新实例。有一个优点是可以给它们任何带有清晰描述的名称。不幸的是,由于它们是静态的,很难嘲笑它们。

    如果可测试性是目标,则可以开发单独的工厂。

    Factories have several benefits over constructors

    • 工厂可以告诉他们正在创建的对象
    • 工厂是多态的,因为它们可以返回正在创建的对象的对象或任何子类型。
    • 如果正在创建的对象有很多可选参数,我们可以将Builder对象作为Factory
      

    如果我可以使用工厂来创建具有标识的新实例,那将是很好的,但这些工厂是否仍然需要调用这些带有公共身份的构造函数?

    我认为除非你使用反射,否则无论如何都需要public

    可能有another solution。您的实体可以apply代替公共构造函数,而commnd或“规范”具有id值。

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

    但我在“实体”类型上预先设置了static方法,因为调用它并不是那么明显。

    我不认为公共建设者是邪恶的。在许多地方调用构造函数并在其中添加新参数导致无限的编译错误修复时,这是一种邪恶。我建议您控制调用域实体的构造函数的位置。

答案 2 :(得分:0)

我的感觉是null id满足身份 - 即,这是 new 潜在的实体。我会使用一个构造函数如下:

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

答案 3 :(得分:0)

最简单的解决方案是使所有构造函数的Id为内部,这需要最少的更改。

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