Строго типизированные идентификаторы

Сущности обычно имеют целочисленные идентификаторы GUID или строковые идентификаторы, поскольку эти типы напрямую поддерживаются базами данных. Однако, если все ваши объекты имеют идентификаторы одного типа, становится довольно легко смешать их и использовать идентификатор продукта, где ожидался идентификатор заказа. На самом деле это довольно частый источник ошибок.

public void AddProductToOrder(int orderId, int productId, int count)
{
    ...
}

...

// Oops, the arguments are swapped!
AddProductToOrder(productId, orderId, int count);

Приведенный выше код компилируется нормально, но, вероятно, не будет работать правильно во время выполнения …

К счастью, есть решение этой проблемы: строго типизированные идентификаторы. Идея проста: объявить определенный тип для идентификатора каждой сущности. В применении к предыдущему примеру код теперь будет выглядеть так:

// Strongly-typed ids instead of int
public void AddProductToOrder(OrderId orderId, ProductId productId, int count)
{
    ...
}

...

// Oops, the arguments are swapped!
AddProductToOrder(productId, orderId, int count);

В приведенном выше коде мы допустили ту же ошибку, что и в первом примере (поменяли местами productId и orderId), но в этом случае типы разные, поэтому компилятор обнаруживает ошибку и сообщает об ошибке. Нам все еще нужно это исправить, но, по крайней мере, эта ошибка вылезла не на продакшене!

Написание строго типизированного идентификатора

Эндрю Лок написал в своем блоге очень полную серию статей о строго типизированных идентификаторах, которые я настоятельно рекомендую вам прочитать. Суть примерно такая:

public readonly struct ProductId : IEquatable<ProductId>
{
    public ProductId(int value)
    {
        Value = value;
    }
    
    public int Value { get; }

    public bool Equals(ProductId other) => other.Value == Value;
    public override bool Equals(object obj) => obj is ProductId other && Equals(other);
    public override int GetHashCode() => Value.GetHashCode();
    public override string ToString() => $"ProductId {Value}";
    public static bool operator ==(ProductId a, ProductId b) => a.Equals(b);
    public static bool operator !=(ProductId a, ProductId b) => !a.Equals(b);
}

Здесь нет ничего сложного, но давайте будем честными: немного больно писать это для каждого объекта в вашей модели. В своей серии статей Эндрю представляет библиотеку для автоматической генерации этого (и не только) кода для вас. Другой вариант — использовать новые генераторы исходного кода C# 9 для достижения того же результата. Но на самом деле C# 9 также предоставляет еще одну функцию, которая может быть даже лучше для этой работы …

Типы записей

Типы записей — это ссылочные типы со встроенной семантикой неизменяемости и значений. Они автоматически предоставляют реализации для всех членов, которые мы написали вручную в предыдущем фрагменте кода (Equals, GetHashCode и т. д.), И предлагают очень сжатый синтаксис, известный как позиционные записи. Если мы перепишем наш тип ProductId, используя записи, мы получим следующее:

public record ProductId(int Value);

Да, вы все правильно прочитали, это всего лишь одна строчка, причем короткая. И она делает все, что делала наша ручная реализация (на самом деле, даже немного больше!).

Основное отличие заключается в следующем: наша ручная реализация была структурой, то есть типом значения, но записи являются ссылочными типами, что означает, что они могут быть нулевыми. Это может не быть серьезной проблемой, особенно если вы используете ссылочные типы, допускающие значение NULL, но об этом следует помнить.

Внезапно написание строго типизированного идентификатора для каждой сущности в нашей модели перестало быть сложной задачей; мы получаем преимущества строгой типизации почти бесплатно. Конечно, есть и другие вопросы, которые следует учитывать, такие как сериализация JSON, использование с Entity Framework Core и т. Д., Но это уже история для другого поста!