如何在不违反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(文件、数据库都不行),它得是内存里的纯逻辑,和外部资源完全解耦。正确姿势是:
把读取逻辑扔到基础设施层(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; } }
- 先在Domain层定义个抽象接口,比如
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读的还是数据库读的,只关心用规则验证数据,符合依赖倒置原则。
- 把验证逻辑抽成Domain服务(或者在Item类里注入这个接口),别直接硬编码规则:
应用层负责绑定依赖
- 在应用层(比如Startup类)的DI容器里注册接口和实现,把XML路径传进去:
services.AddSingleton<IItemValidationRuleProvider>(new XmlItemValidationRuleProvider("Configs/ItemValidation.xml")); services.AddSingleton<ItemValidator>();
- 在应用层(比如Startup类)的DI容器里注册接口和实现,把XML路径传进去:
二、除了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
相关产品推荐
相关产品推荐

