You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在不违反DDD原则下组织实体验证?含XML存储疑问

问题

我已决定采用Execute / CanExecute模式进行实体验证,同时希望将验证值(字符串长度、提示消息等)迁移至XML文件,但不确定在Domain层中加入XmlReader逻辑是否符合DDD原则(刚接触DDD)。

请问根据DDD规范,读取XML文件的逻辑应放置在何处?能否推荐其他存储验证数据的方式?

示例代码

Item类代码

public class Item : Entity, IAggregateRoot
{
    public string IdentityGuid { get; private set; }

    public string Name { get; private set; }

    public string Description { get; private set; }

    public decimal Price { get; private set; }

    public string PictureFileName { get; private set; }

    public string PictureUri { get; private set; }

    public string CatalogTypeId { get; private set; }

    public CatalogType CatalogType { get; private set; }

    public string BrandId { get; private set; }

    public Brand Brand { get; private set; }

    // Quantity in stock
    public int AvailableStock { get; private set; }

    // Available stock at which we should reorder
    public int RestockThreshold { get; private set; }


    // Maximum number of units that can be in-stock at any time (due to physicial/logistical constraints in warehouses)
    public int MaxStockThreshold { get; private set; }

    /// <summary>
    /// True if item is on reorder
    /// </summary>
    public bool OnReorder { get; private set; }

    public Item(
        string name,
        string description,
        decimal price,
        string pictureFileName,
        string pictureUri,
        string catalogTypeId,
        CatalogType catalogType,
        string brandId,
        Brand brand,
        int availableStock,
        int restockThreshold,
        int maxStockThreshold,
        bool onReorder)
    {
        if (CanCreateItem(name, description, price, pictureFileName,
            pictureUri, catalogTypeId, catalogType, brandId, brand,
            availableStock, restockThreshold, maxStockThreshold).Any())
        {
            throw new CatalogDomainException($"Unable to create object {typeof(Item)} data is invalid.");
        }

        Name = name;
        Description = description;
        Price = price;
        PictureFileName = pictureFileName;
        PictureUri = pictureUri;
        CatalogTypeId = catalogTypeId;
        CatalogType = catalogType;
        BrandId = brandId;
        Brand = brand;
        AvailableStock = availableStock;
        MaxStockThreshold = maxStockThreshold;
        OnReorder = onReorder;
    }

    /// <summary>
    /// Validation for Creation of Item. Execute / CanExecute pattern
    /// <param name="name"></param>
    /// <param name="description"></param>
    /// <param name="price"></param>
    /// <param name="pictureFileName"></param>
    /// <param name="pictureUri"></param>
    /// <param name="catalogTypeId"></param>
    /// <param name="catalogType"></param>
    /// <param name="brandId"></param>
    /// <param name="brand"></param>
    /// <param name="availableStock"></param>
    /// <param name="restockThreshold"></param>
    /// <param name="maxStockThreshold"></param>
    /// <returns>Collection of string error message</returns>
    /// </summary>
    public IReadOnlyList<string> CanCreateItem(string name,
        string description,
        decimal price,
        string pictureFileName,
        string pictureUri,
        string catalogTypeId,
        CatalogType catalogType,
        string brandId,
        Brand brand,
        int availableStock,
        int restockThreshold,
        int maxStockThreshold
        )
    {
        var errors = new List<string>();

        // name
        if (string.IsNullOrEmpty(name))
            errors.Add("Name must be at least three characters long.");
        if (name.Length < 3)
            errors.Add("Name must be at least three characters long.");
        if (name.Length > 50)
            errors.Add("A name cannot be more than fifty characters long.");

        // description
        if (string.IsNullOrEmpty(description))
            errors.Add("Description must be at least fifth characters long.");
        if (name.Length < 5)
            errors.Add("Name must be at fifth three characters long.");
        if (name.Length > 1000)
            errors.Add("A name cannot be more than thousand characters long.");

        // price
        if (price < 0)
            errors.Add("Price must be greater than zero.");

        // pictureFileName
        if (string.IsNullOrEmpty(pictureFileName))
            errors.Add("PictureFileName cannot be empty.");

        // pictureUri
        if (string.IsNullOrEmpty(pictureUri))
            errors.Add("PictureUri cannot be empty.");

        // catalogTypeId
        if (string.IsNullOrEmpty(pictureUri))
            errors.Add("CatalogTypeId cannot be empty.");

        // catalogType
        if (string.IsNullOrEmpty(catalogTypeId))
            errors.Add("CatalogTypeId cannot be у.");

        // catalogType
        if (catalogType == null)
            errors.Add("CatalogType cannot be null.");

        // brandId
        if (string.IsNullOrEmpty(brandId))
            errors.Add("BrandId cannot be empty.");

        // brand
        if (brand == null)
            errors.Add("Brand cannot be null.");

        // availableStock
        if (availableStock < 0)
            errors.Add("AvailableStock must be Zero or greater.");

        // restockThreshold
        if (restockThreshold < 0)
            errors.Add("RestockThreshold must be Zero or greater.");

        // maxStockThreshold
        if (maxStockThreshold < 0)
            errors.Add("MaxStockThreshold must be Zero or greater.");

        return errors;
    }
}

验证数据XML示例

<ItemValidationData>
  <ItemProperty>
    <Name>Name</Name>
    <ConstraintName>MinNameLength</ConstraintName>
    <Value>3</Value>
    <Message>Name must be at least three characters long.</Message>
  </ItemProperty>
  <ItemProperty>
    <Name>Name</Name>
    <ConstraintName>MaxNameLength</ConstraintName>
    <Value>50</Value>
    <Message>A name cannot be more than fifty characters long.</Message>
  </ItemProperty>
</ItemValidationData>

解答

一、XML读取逻辑该放哪儿?(严格遵循DDD)

别把XmlReader塞到Domain层!Domain层的核心是纯业务规则,不碰任何外部IO(文件、数据库都不行),它得是内存里的纯逻辑,和外部资源完全解耦。正确姿势是:

  1. 把读取逻辑扔到基础设施层(Infrastructure Layer)

    • 先在Domain层定义个抽象接口,比如IItemValidationRuleProvider,用来给Domain层提供验证规则数据,别暴露具体读取方式:
      public interface IItemValidationRuleProvider
      {
          // 按属性名分组返回验证规则
          Dictionary<string, List<ValidationRule>> GetItemRules();
      }
      
      public class ValidationRule
      {
          public string RuleType { get; set; } // 比如MinLength、MaxLength
          public object Value { get; set; }
          public string ErrorMessage { get; set; }
      }
      
    • 然后在基础设施层写实现类,专门处理XML读取:
      public class XmlItemValidationRuleProvider : IItemValidationRuleProvider
      {
          private readonly string _xmlPath;
      
          public XmlItemValidationRuleProvider(string xmlPath) => _xmlPath = xmlPath;
      
          public Dictionary<string, List<ValidationRule>> GetItemRules()
          {
              // 这里写XmlReader解析XML的逻辑,把节点转成ValidationRule集合
              var rules = new Dictionary<string, List<ValidationRule>>();
              // 读取XML文件,遍历每个ItemProperty,按Name分组
              return rules;
          }
      }
      
  2. Domain层只依赖抽象接口

    • 把验证逻辑抽成Domain服务(或者在Item类里注入这个接口),别直接硬编码规则:
      public class ItemValidator
      {
          private readonly IItemValidationRuleProvider _ruleProvider;
      
          public ItemValidator(IItemValidationRuleProvider ruleProvider) => _ruleProvider = ruleProvider;
      
          public IReadOnlyList<string> ValidateCreation(string name, string description, /* 其他参数 */)
          {
              var errors = new List<string>();
              var rules = _ruleProvider.GetItemRules();
      
              // 验证Name属性
              if (rules.TryGetValue("Name", out var nameRules))
              {
                  foreach (var rule in nameRules)
                  {
                      switch (rule.RuleType)
                      {
                          case "MinLength":
                              if (string.IsNullOrEmpty(name) || name.Length < (int)rule.Value)
                                  errors.Add(rule.ErrorMessage);
                              break;
                          case "MaxLength":
                              if (name.Length > (int)rule.Value)
                                  errors.Add(rule.ErrorMessage);
                              break;
                      }
                  }
              }
      
              // 其他属性的验证逻辑照猫画虎
              return errors;
          }
      }
      
    • 这样Domain层完全不知道规则是从XML读的还是数据库读的,只关心用规则验证数据,符合依赖倒置原则。
  3. 应用层负责绑定依赖

    • 在应用层(比如Startup类)的DI容器里注册接口和实现,把XML路径传进去:
      services.AddSingleton<IItemValidationRuleProvider>(new XmlItemValidationRuleProvider("Configs/ItemValidation.xml"));
      services.AddSingleton<ItemValidator>();
      

二、除了XML,还有哪些存储验证数据的方式?

根据业务场景选:

1. JSON/YAML配置文件

  • 比XML简洁太多,.NET里直接用IConfiguration就能绑定到实体类,不用手写解析逻辑:
    {
      "ItemValidation": {
        "Name": [
          {
            "RuleType": "MinLength",
            "Value": 3,
            "ErrorMessage": "Name must be at least 3 characters long."
          },
          {
            "RuleType": "MaxLength",
            "Value": 50,
            "ErrorMessage": "Name can't exceed 50 characters."
          }
        ]
      }
    }
    
  • 读取逻辑同样放基础设施层,用IConfiguration.GetSection("ItemValidation").Get<...>()直接转成规则对象,省事。

2. 数据库存储

  • 如果规则需要动态改(比如运营要随时调整字段长度限制),就把规则存数据库。
  • 同样在基础设施层实现IItemValidationRuleProvider,从数据库查数据,Domain层完全感知不到。

3. 代码静态配置(适合规则稳定的场景)

  • 要是规则几乎不会变,直接在Domain层写个静态类存规则,不用碰外部文件:
    public static class ItemValidationRules
    {
        public static readonly List<ValidationRule> NameRules = new()
        {
            new() { RuleType = "MinLength", Value = 3, ErrorMessage = "Name must be at least 3 characters long." },
            new() { RuleType = "MaxLength", Value = 50, ErrorMessage = "Name can't exceed 50 characters." }
        };
    
        // 其他属性规则...
    }
    
  • 这种方式最直接,Domain层没有外部依赖,性能也最好。

4. 数据注解/FluentValidation

  • 用.NET自带的数据注解,直接贴在实体属性上:
    public class Item : Entity, IAggregateRoot
    {
        [Required(ErrorMessage = "Name can't be empty.")]
        [StringLength(50, MinimumLength = 3, ErrorMessage = "Name must be between 3 and 50 characters.")]
        public string Name { get; private set; }
    
        // 其他属性...
    }
    
  • 配合FluentValidation库的话,能写更复杂的规则,而且验证逻辑和实体绑定,Domain层依然纯净。

内容的提问来源于stack exchange,提问作者Vlad

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 08:40:32