Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Подсказка
Это фрагмент из электронной книги «Архитектура микрослужб .NET для контейнеризованных приложений .NET», доступной в документации .NET или в виде бесплатного скачиваемого PDF-файла, который можно прочитать в автономном режиме.
Как описано в предыдущих разделах о сущностях и агрегатах, идентификация является основой для сущностей. Однако в системе существует множество объектов и элементов данных, которые не требуют отслеживания и идентификации, таких как объекты-значения.
Объект значения может ссылаться на другие сущности. Например, в приложении, создающем маршрут, описывающий, как получить от одной точки к другой, этот маршрут будет объектом значения. Это было бы изображение точек на определенном маршруте, но этот предлагаемый маршрут не будет иметь идентичности, хотя внутренне оно может относиться к сущностям, например, таким как город или дорога.
На рисунке 7–13 показан объект значения Address в агрегате Order.
Рис. 7-13. Объект значения адреса в агрегате 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
не имеет идентифицирующего поля, и поэтому поле идентификатора не определено ни в определении класса 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 и более поздних версий
Вы только что узнали, как определить объект value в модели домена. Но как персистировать его в базе данных с помощью 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 с полем идентификатора. Затем, чтобы он выглядел как объект со значением без идентичности, можно скрыть его идентификатор, чтобы для ясного понимания показать, что идентичность объекта со значением не важна в доменной модели. Этот идентификатор можно скрыть, используя его в качестве теневого свойства. Так как эта конфигурация для скрытия идентификатора в модели настроена на уровне инфраструктуры EF, она будет прозрачной для модели домена.
В начальной версии eShopOnContainers (.NET Core 1.1) скрытый идентификатор, необходимый инфраструктуре EF Core, был реализован следующим образом на уровне DbContext с помощью API Fluent в проекте инфраструктуры. Таким образом, идентификатор был скрыт с точки зрения модели домена, но все еще присутствует в инфраструктуре.
// 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 и более поздних версиях).
Например, в модели домена заказа в eShopOnContainers в рамках сущности Order объект-значение Address реализуется как принадлежащий тип сущности внутри сущности владельца, которой является сущность 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
.
По умолчанию соглашения EF Core называют столбцы базы данных по свойствам типа сущности, которым они принадлежат, как EntityProperty_OwnedEntityProperty
. Таким образом, внутренние свойства Address
будут отображаться в Orders
таблице с именами Address_Street
( Address_City
и т. д. для State
, Country
и ZipCode
).
Можно присоединить метод Property().HasColumnName()
Fluent для переименования этих столбцов. В случае, когда 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; }
}
Дополнительные сведения о типах принадлежащих сущностей
Собственные типы задаются при конфигурировании свойства навигации для определенного типа с помощью Fluent API OwnsOne.
Определение владеющего типа в нашей модели метаданных состоит из: типа владельца, свойства навигации и CLR-типа владеющего типа.
Идентификатор (ключ) экземпляра подчиненного типа в нашем стеке — это комбинация идентификатора типа владельца и определения подчиненного типа.
Возможности подконтрольных сущностей
Принадлежащие типы могут ссылаться на другие сущности, принадлежащие (вложенные типы) или не принадлежащие (обычные ссылочные свойства навигации другим сущностям).
Вы можете сопоставить один и тот же тип CLR как разные типы обладания в одной сущности владельца через отдельные свойства навигации.
Разделение таблиц настраивается по соглашению, но вы можете отказаться, сопоставив принадлежащий тип с другой таблицей с помощью ToTable.
Страстная загрузка выполняется автоматически на собственных типах, т. е. не требуется вызывать запрос
.Include()
.Можно настроить с помощью атрибута
[Owned]
, используя EF Core 2.1 и более поздних версий.Может обрабатывать коллекции собственных типов (с помощью версии 2.2 и более поздних версий).
Ограничения принадлежащих объектов
Невозможно создать
DbSet<T>
владеемого типа (преднамеренно).Вы не можете вызывать
ModelBuilder.Entity<T>()
на принадлежащих типах (в настоящее время по задумке).Отсутствует поддержка необязательных (то есть допускающих значение NULL) типов, принадлежащих владельцу и сопоставленных с ним в одной таблице (т. е. с разделением таблицы). Это связано с тем, что сопоставление выполняется для каждого свойства, и нет отдельного индикатора для нулевого комплексного значения в целом.
Поддержка сопоставления наследования для типов владения не предлагается, но вы сможете сопоставить два конечных типа той же иерархии наследования как разные типы владения. EF Core не будет думать о том, что они являются частью той же иерархии.
Основные различия с сложными типами EF6
- Разделение таблиц является необязательным, то есть они могут быть сопоставлены с отдельной таблицей и по-прежнему принадлежать типам.
Дополнительные ресурсы
Мартин Фаулер. Шаблон ValueObject
https://martinfowler.com/bliki/ValueObject.htmlЭрик Эванс. Domain-Driven дизайн: решение сложности в сердце программного обеспечения. (Книга; включает обсуждение объектов значений)
https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215/Вон Вернон (Vaughn Vernon). Реализация дизайна Domain-Driven. (Книга; включает обсуждение объектов значений)
https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577/Типы принадлежащих сущностей
https://learn.microsoft.com/ef/core/modeling/owned-entitiesСвойства тени
https://learn.microsoft.com/ef/core/modeling/shadow-propertiesСложные типы и/или объекты значений. Обсуждение в репозитории EF Core GitHub (вкладка "Проблемы")
https://github.com/dotnet/efcore/issues/246ValueObject.cs. Класс объектов базового значения в eShopOnContainers.
https://github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/Services/Ordering/Ordering.Domain/SeedWork/ValueObject.csValueObject.cs. Класс объектов базового значения в CSharpFunctionalExtensions.
https://github.com/vkhorikov/CSharpFunctionalExtensions/blob/master/CSharpFunctionalExtensions/ValueObject/ValueObject.csКласс Адрес. Пример класса объекта-значения в eShopOnContainers.
https://github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Address.cs