强类型 ID 是自定义类型,用于表示应用程序中的实体标识符 (ID),而不是使用 int、Guid 或 string 等基元类型。您不是直接使用这些基元类型来表示 ID,而是创建一个封装 ID 值的特定类或结构。此方法有助于使代码更具表现力、更安全且更易于维护。
简而言之,Primitive Obsession 是一种使用基本数据类型来表示更复杂的概念的趋势。当基本数据类型(如 int、string 或 DateTime)被过度使用来表示域中的复杂概念时,就会出现 Primitive obsession。
这是一种常见的反模式,可能导致代码不清晰和系统更难维护。
如果项目中有多个实体,则标识符 (ID) 也会遇到同样的问题。如果您使用所有实体标识符(如 CustomerId、OrderId、OrderItemId),则可能会导致错误地传递 a 所在的位置。这一切都是因为到处都使用相同的类型。GuidOrderIdOrderItemId
这个原始的痴迷问题可以通过使用 Value Objects 来解决。值对象表示域中没有标识但由其属性定义的值。
实体标识符的原始痴迷问题是通过使用强类型 ID 来解决的,强类型 ID 是一种 Value Objects,但仅适用于实体标识符。
Primitive Obsession with Entity Identifiers 示例
让我们探索一个具有 and 实体的应用程序示例。这两个实体都有 guid 作为其实体标识符类型:OrderOrderItem
public class Order
{
public Guid Id { get; set; }
}
public class OrderItem
{
public Guid Id { get; set; }
}
假设您正在调用该方法:ProcessOrderAsync
public Task ProcessOrderAsync(Guid orderId, Guid orderItemId)
{
// Logic to process the order and its item
}
// Correct usage
ProcessOrder(order.Id, orderItem.Id);
// Incorrect usage - No compile-time error
ProcessOrder(orderItem.Id, order.Id);
此代码将成功编译并执行。您是否注意到问题?
由于我们对 和 - 都使用 type,因此您可以按错误的顺序传递参数。您最终会在数据库中得到错误的数据,这可能会导致严重的问题。GuidOrder.IdOrderItem.Id
此问题的解决方案是使用强类型 ID,它将实体标识符封装到一个自定义的有意义的单元中。
什么是强类型 ID?
强类型 ID 是自定义类型,用于表示应用程序中的实体标识符 (ID),而不是使用 int、Guid 或 string 等基元类型。您不是直接使用这些基元类型来表示 ID,而是创建一个封装 ID 值的特定类或结构。此方法有助于使代码更具表现力、更安全且更易于维护。
强类型 ID 的主要特征:
不可变性:强类型 ID 一旦创建,就无法更改。任何修改都会导致新实例。
相等性:强类型 ID 根据其值(而不是引用)进行比较。
强类型 ID 的优点:
**增强的类型安全性:**强类型 ID 的最显著好处之一是它们增强了类型安全性。这减少了意外混淆不同类型 ID 的可能性,从而减少了错误和运行时错误。
**改进的代码清晰度:**强类型 ID 使您的代码更具表现力和自文档性。当您看到接受 OrderId 的方法时,可以立即清楚地知道需要哪种 ID,而不是通用的 Guid 或 int。
**更好的域建模:**在域驱动设计 (DDD) 中,强类型 ID 有助于强化实体及其标识的概念。这使得域模型更加健壮,并与它所代表的实际概念保持一致。
**支持未来增强功能:**如果 ID 要求发生更改(例如,需要更改实体标识符的类型),则可以更新或扩展强类型 ID 类以支持这些新要求,而不会破坏现有代码。
**更轻松的重构:**强类型 ID 使重构更容易,因为可以在单个位置对 ID 相关逻辑进行更改,从而减少在此过程中引入错误的可能性。
现在,让我们探讨一下在 .NET 中创建强类型 ID 的选项。
示例应用程序
今天,我将向您展示如何为负责创建和更新已订购产品的客户、订单和发货的 Shipping 应用程序实施强类型 ID。
此应用程序具有以下实体:
客户
Orders、OrderItems
Shipments, ShipmentItems
我正在为我的实体使用域驱动设计实践。让我们来探索一下使用基元类型作为实体标识符的 an 和 entities:OrderOrder Item
public class Order
{
public Guid Id { get; private set; }
public OrderNumber OrderNumber { get; private set; }
public Guid CustomerId { get; private set; }
public Customer Customer { get; private set; }
public DateTime Date { get; private set; }
public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
private readonly List<OrderItem> _items = new();
private Order() { }
public static Order Create(OrderNumber orderNumber, Customer customer, List<OrderItem> items)
{
var order = new Order
{
Id = Guid.NewGuid(),
OrderNumber = orderNumber,
Customer = customer,
CustomerId = customer.Id,
Date = DateTime.UtcNow
};
order.AddItems(items);
return order;
}
private Order AddItems(List<OrderItem> items)
{
_items.AddRange(items);
return this;
}
}
public class OrderItem
{
public Guid Id { get; private set; }
public ProductName Product { get; private set; }
public int Quantity { get; private set; }
public Guid OrderId { get; private set; }
public Order Order { get; private set; } = null!;
private OrderItem() { }
public OrderItem(ProductName productName, int quantity)
{
Id = Guid.NewGuid();
Product = productName;
Quantity = quantity;
}
}
如您所见,这两个实体都使用 Value Objects 作为其属性。如果您想了解有关 Value Objects 的更多信息,请务必查看我相应的博客文章。
现在,让我们探讨如何将实体标识符的这些基元类型替换为强类型 ID。
在 .NET 中创建强类型 ID
我可以列出以下用于创建强类型 ID 的最常用选项:
使用 Andrew Lock 编写的 StronglyTypedId 包
使用 C# 记录
使用 C# 记录结构
让我们更深入地探讨每个选项。
使用 StronglyTypedId 包创建强类型 ID
通过使用该包,您可以为实体生成强类型 ID 结构。StronglyTypedId
首先,您需要安装软件包:
dotnet add package StronglyTypedId
您需要定义 a 并添加 attribute:partial structStronglyTypedId
[StronglyTypedId]
public readonly partial struct OrderId { }
[StronglyTypedId]
public readonly partial struct OrderItemId { }
StronglyTypedIdpackage 将使用源生成器来实现 AND 结构体。默认情况下,下划线类型为 .OrderIdOrderItemIdGuid
以下是创建 OrderId 的方法:
var orderId = new OrderId(Guid.NewGuid());
var orderItemId = new OrderItemId(Guid.NewGuid());
您可以使用属性来检索隐藏在强类型 ID 中的值:Value
var guid = orderId.Value;
var guid2 = orderItemId.Value;
如果需要更改类型,请在属性中指定 a:Template
[StronglyTypedId(Template.Int)]
public readonly partial struct OrderId { }
当前支持的内置支持类型包括:
Guid (默认)
int
long
string
使用记录创建强类型 ID
您不必使用外部包,因为 C# 记录已经具有强类型 ID 所需的一切。
记录是在 .NET 中创建强类型 ID 的一种非常现代的方法。
记录是不可变的引用类型,它们支持开箱即用的相等比较。它们是根据其属性而不是引用进行比较的。
以下是使用记录定义相同的强类型 ID 的方法:
public record OrderId(Guid Value);
public record OrderItemId(Guid Value);
使用记录结构创建强类型 ID
记录是强类型 ID 的绝佳选择,但它们是引用类型。如果您关心内存分配,则可以用于强类型 ID。它们的行为与 None 相同,但它们是值类型,而不是在堆上分配。readonly record structsrecords
这是我个人创建强类型 ID 的选择。
以下是定义强类型 ID 的方法:record structs
public readonly record struct OrderId(Guid Value);
public readonly record struct OrderItemId(Guid Value);
以下是 and 实体使用强类型 ID 时的样子:OrderOrderItem
public class Order
{
public OrderId Id { get; private set; }
public OrderNumber OrderNumber { get; private set; }
public CustomerId CustomerId { get; private set; }
public Customer Customer { get; private set; }
public DateTime Date { get; private set; }
public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
private readonly List<OrderItem> _items = new();
private Order() { }
public static Order Create(OrderNumber orderNumber, Customer customer, List\<OrderItem> items)
{
var order = new Order
{
Id = new OrderId(Guid.NewGuid()),
OrderNumber = orderNumber,
Customer = customer,
CustomerId = customer.Id,
Date = DateTime.UtcNow
};
order.AddItems(items);
return order;
}
private Order AddItems(List<OrderItem> items)
{
_items.AddRange(items);
return this;
}
}
public class OrderItem
{
public OrderItemId Id { get; private set; }
public ProductName Product { get; private set; }
public int Quantity { get; private set; }
public OrderId OrderId { get; private set; }
public Order Order { get; private set; } = null!;
private OrderItem() { }
public OrderItem(ProductName productName, int quantity)
{
Id = new OrderItemId(Guid.NewGuid());
Product = productName;
Quantity = quantity;
}
}
如果您尝试滥用实体标识符,您将收到编译错误:
在 EF Core 中映射强类型 ID
在实体模型中引入强类型 ID 后,需要修改 EF Core 映射。
需要使用 conversion 来告知 EF Core 如何将强类型 ID 映射到数据库,以及如何将数据库值映射到 ID。
例如,对于 entity:Order
builder.Property(x => x.Id)
.HasConversion(
id => id.Value,
value => new OrderId(value)
)
.IsRequired();
强类型 ID 和请求/响应/DTO 模型
强类型 ID 是特定于域的模型,外界不应知道它们。此外,您的公共请求/响应/DTO 模型应尽可能简单。
最好在请求/响应/DTO 模型中使用普通基元类型,并将它们映射到域实体,反之亦然。
例如,我在 “Create Order” 用例中使用 type 映射到:CustomerIdGuidCustomerId
public sealed record CreateOrderRequest(
Guid CustomerId,
List<OrderItemRequest> Items,
Address ShippingAddress,
string Carrier,
string ReceiverEmail);
public static CreateOrderCommand MapToCommand(this CreateOrderRequest request)
{
return new CreateOrderCommand(
CustomerId: new CustomerId(request.CustomerId),
Items: request.Items,
ShippingAddress: request.ShippingAddress,
Carrier: request.Carrier,
ReceiverEmail: request.ReceiverEmail
);
}
以及从 Strong Typed 到 的反向映射:CustomerResponseOrderId
public static OrderResponse MapToResponse(this Order order)
{
return new OrderResponse(
OrderId: order.Id.Value,
OrderNumber: order.OrderNumber.Value,
OrderDate: order.Date,
Items: order.Items.Select(x => new OrderItemResponse(
ProductName: x.Product.Value,
Quantity: x.Quantity)).ToList()
);
}
强类型 ID 是提高 .NET 应用程序的类型安全性、清晰度和可维护性的强大工具。通过将 ID 封装在专用记录或结构中,可以防止常见错误并使代码更具表现力。
C# 记录和 readonly 记录结构为您提供了一种优雅、简单、快速的方式来实现强类型 ID,而无需样板代码。
点击下方卡片关注DotNet NB
一起交流学习
▲ 点击上方卡片关注DotNet NB,一起交流学习
请在公众号后台