Реализация объектов значений

Совет

Это содержимое является фрагментом из электронной книги, архитектуры микрослужб .NET для контейнерных приложений .NET, доступных в документации .NET или в виде бесплатного скачиваемого PDF-файла, который можно читать в автономном режиме.

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

Как отмечалось в предыдущих разделах о сущностях и статистических выражениях, удостоверение — это базовый компонент сущностей. Тем не менее в системе существует множество объектов и элементов данных, не требующих удостоверений и их отслеживания, такие как объекты значений.

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

На рисунке 7-13 изображен объект значения Address (адрес) в статистическом выражении Order (заказ).

Diagram showing the Address value-object inside the Order Aggregate.

Рис. 7-13. Объект значения Address (адрес) в статистическом выражении Order (заказ)

Как показано на рисунке 7-13, сущность обычно состоит из нескольких атрибутов. Например, сущность Order можно смоделировать как сущность с удостоверением и внутренне состоящей из набора свойств, таких как OrderId, OrderDate, OrderItems и т. д. Но адрес, который является просто сложным значением, состоящим из страны или региона, улицы, города и т. д. и не имеющим удостоверения в этом домене, необходимо моделировать и рассматривать как объект значения.

Важные характеристики объектов значений

У объектов значений две основные характеристики:

  • У них нет удостоверения.

  • Они неизменяемы.

Первая характеристика уже обсуждались. Неизменяемость является важным требованием. После создания объекта значения он должен оставаться неизменяемым. Таким образом, при создании объекта необходимо задать необходимые значения, и они не должны изменяться в течение всего времени существования объекта.

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

Реализация объекта Value в C#

С точки зрения реализации можно создать базовый класс объекта значения, имеющий основные служебные методы, такие как сравнение, основанное на сравнении всех атрибутов (поскольку объект значения не должен задаваться удостоверением) и других основных характеристик. Следующий пример показывает создание базового класса объекта значения, используемого в микрослужбе заказов из eShopOnContainers.

public abstract class ValueObject
{
    protected static bool EqualOperator(ValueObject left, ValueObject right)
    {
        if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))
        {
            return false;
        }
        return ReferenceEquals(left, right) || left.Equals(right);
    }

    protected static bool NotEqualOperator(ValueObject left, ValueObject right)
    {
        return !(EqualOperator(left, right));
    }

    protected abstract IEnumerable<object> GetEqualityComponents();

    public override bool Equals(object obj)
    {
        if (obj == null || obj.GetType() != GetType())
        {
            return false;
        }

        var other = (ValueObject)obj;

        return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
    }

    public override int GetHashCode()
    {
        return GetEqualityComponents()
            .Select(x => x != null ? x.GetHashCode() : 0)
            .Aggregate((x, y) => x ^ y);
    }
    // Other utility methods
}

ValueObject является типом abstract class, но в данном примере он не перегружает операторы == и !=. Это можно сделать, сравнив делегата с переопределением Equals. Например, рассмотрим следующую перегрузку оператора для типа ValueObject:

public static bool operator ==(ValueObject one, ValueObject two)
{
    return EqualOperator(one, two);
}

public static bool operator !=(ValueObject one, ValueObject two)
{
    return NotEqualOperator(one, two);
}

Этот класс можно использовать при реализации реального объекта значения, как это показано на примере объекта значения Address в следующем коде:

public class Address : ValueObject
{
    public String Street { get; private set; }
    public String City { get; private set; }
    public String State { get; private set; }
    public String Country { get; private set; }
    public String ZipCode { get; private set; }

    public Address() { }

    public Address(string street, string city, string state, string country, string zipcode)
    {
        Street = street;
        City = city;
        State = state;
        Country = country;
        ZipCode = zipcode;
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        // Using a yield return statement to return each element one at a time
        yield return Street;
        yield return City;
        yield return State;
        yield return Country;
        yield return ZipCode;
    }
}

У этой реализации объекта значения для Address нет удостоверения, а следовательно, и поля ID, ни в определении класса Address, ни даже в определении класса ValueObject.

Отсутствие поля ИД в классе, используемом Entity Framework (EF), было недопустимым до выпуска EF Core 2.0. Эта возможность помогает реализовывать более эффективные объекты значений без ИД. Именно это пояснение используется в следующем разделе.

Можно возразить, что объекты значений, являясь неизменяемыми, должны быть доступны только для чтения (например, свойства, отвечающие только за получение), и это действительно так. Однако объекты значений обычно сериализируются и десериализируются для прохождения очередей сообщений, а наличие доступа только для чтения не позволяет десериализатору назначать значения, поэтому просто оставляем их как private set, доступный только для чтения, чего хватает для практического применения.

Семантика сравнения объектов значений

Для сравнения двух экземпляров типа Address можно использовать следующие методы:

var one = new Address("1 Microsoft Way", "Redmond", "WA", "US", "98052");
var two = new Address("1 Microsoft Way", "Redmond", "WA", "US", "98052");

Console.WriteLine(EqualityComparer<Address>.Default.Equals(one, two)); // True
Console.WriteLine(object.Equals(one, two)); // True
Console.WriteLine(one.Equals(two)); // True
Console.WriteLine(one == two); // True

Если все значения одинаковы, то сравнения оцениваются правильно как true. Если вы не выбрали перегрузку операторов == и !=, то последнее сравнение one == two будет оцениваться как false. Дополнительные сведения см. в разделе Операторы равенства ValueObject перегрузки

Хранение объектов значений в базе данных в EF Core 2.0 и более поздних версий

Мы узнали, как определить объект значения в модели домена. Но как же сохранить его в базе данных через Entity Framework Core, так как он обычно обращается к сущностям по удостоверению?

Общие сведения и устаревший подход с использованием EF Core 1.1

Для справки: при использовании EF Core 1.0 и 1.1 было невозможно использовать сложные типы в том виде, в котором они были определены в EF 6.x в традиционном решении .NET Framework. Таким образом, при использовании EF Core 1.0 и 1.1 необходимо хранить объект значения в виде сущности EF с полем ID. Затем, чтобы сущность больше походила на объект значения, нужно скрыть поле ID, дав понять таким образом, что удостоверение объекта значения не важно в данной модели домена. Можно скрыть поле ID при помощи затемнения свойства. Так как такой вариант скрытия удостоверения в модели задается на уровне инфраструктуры EF, это будет в каком-то смысле прозрачно для модели домена.

В первоначальной версии eShopOnContainers (.NET Core 1.1) скрытие поля ID, необходимое для инфраструктуры EF Core, было реализовано на уровне DbContext при помощи текучего API в инфраструктуре проекта следующим образом. Таким образом, удостоверение было скрыто с точки зрения модели домена, но все еще присутствует в инфраструктуре.

// Old approach with EF Core 1.1
// Fluent API within the OrderingContext:DbContext in the Infrastructure project
void ConfigureAddress(EntityTypeBuilder<Address> addressConfiguration)
{
    addressConfiguration.ToTable("address", DEFAULT_SCHEMA);

    addressConfiguration.Property<int>("Id")  // Id is a shadow property
        .IsRequired();
    addressConfiguration.HasKey("Id");   // Id is a shadow property
}

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

В EF Core 2.0 и более поздних версиях появились новые и лучшие способы хранения объектов значений.

Хранение объектов значений в виде принадлежащих типов сущностей в EF Core 2.0 и более поздних версиях

Даже при некоторых преимуществах канонического шаблона объекта значения в предметно-ориентированном проектировании (DDD) перед принадлежащим типом сущности EF Core сейчас это является наилучшим способом хранения объектов значений в EF Core 2.0 и более поздних версий. Существующие ограничения приведены в конце этого раздела.

Возможность использования принадлежащих типов сущностей была добавлена в EF Core начиная с версии 2.0.

Принадлежащий тип сущности позволяет сопоставить типы, которые не имеют своего удостоверения, определены явным образом в модели домена и используются в качестве свойств, таких как объект значения, в любой сущности. Принадлежащий тип сущности предоставляет другому типу сущности тот же тип среды CLR (в нашем случае это просто обычный класс). Сущность, содержащая определяющую навигацию, является сущностью-владельцем. При запросе владельца по умолчанию включаются принадлежащие типы.

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

Удостоверение экземпляров принадлежащих типов не является полностью их собственным. Оно состоит из трех компонентов:

  • Удостоверение владельца

  • Указывающее на него свойство навигации

  • В случае с коллекциями принадлежащих типов — независимый компонент (поддерживается в EF Core 2.2 и более поздних версиях).

Например, в модели домена Ordering в eShopOnContainers объект значения Address, являющийся частью сущности Order, реализован как принадлежащий тип сущности в рамках сущности-владельца, которой является сущность Order. Address — это тип, не имеющий определенного в модели домена свойства-удостоверения. Он используется как свойство сущности Order для указания адреса доставки конкретного заказа.

По соглашению для принадлежащего типа создается теневой первичный ключ, и этот тип сопоставляется с той же таблицей, что и владелец, с помощью разбиения таблицы. Это позволяет использовать принадлежащие типы аналогично сложным типам в EF6 в традиционном .NET Framework.

Важно отметить, что по соглашению принадлежащие типы никогда не обнаруживаются EF Core, поэтому их необходимо объявлять явно.

В eShopOnContainers в файле OrderingContext.cs в методе OnModelCreating() применяется несколько конфигураций инфраструктуры. Одна из них относится к сущности Order.

// Part of the OrderingContext.cs class at the Ordering.Infrastructure project
//
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.ApplyConfiguration(new ClientRequestEntityTypeConfiguration());
    modelBuilder.ApplyConfiguration(new PaymentMethodEntityTypeConfiguration());
    modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration());
    modelBuilder.ApplyConfiguration(new OrderItemEntityTypeConfiguration());
    //...Additional type configurations
}

В следующем коде инфраструктура сохраняемости определяется для сущности Order:

// Part of the OrderEntityTypeConfiguration.cs class
//
public void Configure(EntityTypeBuilder<Order> orderConfiguration)
{
    orderConfiguration.ToTable("orders", OrderingContext.DEFAULT_SCHEMA);
    orderConfiguration.HasKey(o => o.Id);
    orderConfiguration.Ignore(b => b.DomainEvents);
    orderConfiguration.Property(o => o.Id)
        .ForSqlServerUseSequenceHiLo("orderseq", OrderingContext.DEFAULT_SCHEMA);

    //Address value object persisted as owned entity in EF Core 2.0
    orderConfiguration.OwnsOne(o => o.Address);

    orderConfiguration.Property<DateTime>("OrderDate").IsRequired();

    //...Additional validations, constraints and code...
    //...
}

В приведенном выше коде метод orderConfiguration.OwnsOne(o => o.Address) указывает, что свойство Address принадлежит сущности типа Order.

По умолчанию соглашения EF Core именуют столбцы базы данных для свойств принадлежащего типа сущностей следующим образом: EntityProperty_OwnedEntityProperty. Следовательно, внутренние свойства типа Address будут отображаться в таблице Orders с именами Address_Street, Address_City (и так далее для свойств State, Country и ZipCode).

Можно переименовать эти столбцы, добавив текучий метод Property().HasColumnName(). В случае, когда свойство Address является общедоступным свойством, сопоставление будет выполняться подобным образом:

orderConfiguration.OwnsOne(p => p.Address)
                            .Property(p=>p.Street).HasColumnName("ShippingStreet");

orderConfiguration.OwnsOne(p => p.Address)
                            .Property(p=>p.City).HasColumnName("ShippingCity");

Метод OwnsOne возможно объединять в цепочку для текущего сопоставления. В следующем гипотетическом примере сущность OrderDetails владеет BillingAddress и ShippingAddress, которые принадлежат к типу Address. А OrderDetails, в свою очередь, принадлежит типу Order.

orderConfiguration.OwnsOne(p => p.OrderDetails, cb =>
    {
        cb.OwnsOne(c => c.BillingAddress);
        cb.OwnsOne(c => c.ShippingAddress);
    });
//...
//...
public class Order
{
    public int Id { get; set; }
    public OrderDetails OrderDetails { get; set; }
}

public class OrderDetails
{
    public Address BillingAddress { get; set; }
    public Address ShippingAddress { get; set; }
}

public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
}

Дополнительные сведения о принадлежащих типах сущностей

  • Принадлежащие типы определяются при настройке свойства навигации конкретного типа с помощью текучего API OwnsOne.

  • Определение принадлежащего типа в нашей модели метаданных состоит из следующих элементов: тип владельца, свойство навигации и тип среды CLR принадлежащего типа.

  • Удостоверение (ключ) экземпляра принадлежащего типа в нашем стеке состоит из удостоверения владельца и определения принадлежащего типа.

Возможности принадлежащих сущностей

  • Принадлежащие типы могут ссылаться на другие сущности, как принадлежащие (вложенные принадлежащие типы), так и не принадлежащие (обычные ссылки свойств навигации на другие сущности).

  • Можно сопоставлять одной сущности-владельцу одни и те же типы среды CLR как разные принадлежащие типы с помощью различных свойств навигации.

  • По соглашению выполняется разделение таблицы, однако можно отказаться от этого и сопоставить принадлежащий тип в другую таблицу, используя метод ToTable.

  • Безотложная загрузка для принадлежащих типов выполняется автоматически, т. е. в запросе не нужно вызывать .Include().

  • Можно настроить с помощью атрибута [Owned], используя EF Core 2.1 или более поздней версии.

  • Может выполнять обработку коллекций принадлежащих типов (при использовании версии 2.2 и более поздних).

Ограничения принадлежащих сущностей

  • DbSet<T> нельзя создать из принадлежащих типов (изначально не предусмотрено).

  • Нельзя вызвать ModelBuilder.Entity<T>() для собственных типов (в настоящее время намеренно).

  • Не поддерживаются необязательные (т. е. допускающие значение NULL) принадлежащие типы, которые сопоставляются с владельцем в одной таблице (т. е. с помощью разделения таблицы). Это обусловлено тем, что сопоставление выполняется для каждого свойства и нет отдельной граничной метки для комплексного значения Null в целом.

  • Не поддерживается наследование сопоставления для принадлежащих типов, но мы можем сопоставить два конечных типа одной иерархии наследования как два различных принадлежащих типа. EF Core не будет учитывать тот факт, что они являются частью одной иерархии.

Основные отличия от сложных типов EF6

  • Разделение таблицы является необязательным, т. е. они также могут быть сопоставлены с отдельной таблицей и по-прежнему быть принадлежащими типами.

Дополнительные ресурсы