Value object implementation in C#
In terms of implementation, you can have a value object base class that has basic utility methods like
equality based on the comparison between all the attributes (since a value object must not be based
on identity) and other fundamental characteristics. The following example shows a value object base
class used in the ordering microservice from 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
}
216
CHAPTER 6 | Tackle Business Complexity in a Microservice with DDD and CQRS Patterns
The
ValueObject
is an
abstract class
type, but in this example, it doesn’t overload the
==
and
!=
operators. You could choose to do so, making comparisons delegate to the
Equals
override. For
example, consider the following operator overloads to the
ValueObject
type:
public
static
bool
operator
==(ValueObject one, ValueObject two)
{
return
EqualOperator
(one, two);
}
public
static
bool
operator
!=(ValueObject one, ValueObject two)
{
return
NotEqualOperator
(one, two);
}
You can use this class when implementing your actual value object, as with the
Address
value object
shown in the following example:
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;
}
}
This value object implementation of
Address
has no identity, and therefore no ID field is defined for it,
either in the
Address
class definition or the
ValueObject
class definition.
Having no ID field in a class to be used by Entity Framework (EF) was not possible until EF Core 2.0,
which greatly helps to implement better value objects with no ID. That is precisely the explanation of
the next section.
217
CHAPTER 6 | Tackle Business Complexity in a Microservice with DDD and CQRS Patterns
It could be argued that value objects, being immutable, should be read-only (that is, have get-only
properties), and that’s indeed true. However, value
objects are usually serialized and deserialized to go
through message queues, and being read-only stops the deserializer from assigning values, so you
just leave them as
private set
, which is read-only enough to be practical.
Value object comparison semantics
Two instances of the
Address
type can be compared using all the following methods:
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 .
Default
.
Equals
(one, two));
// True
Console.
WriteLine
(
object
.
Equals
(one, two));
// True
Console.
WriteLine
(one.
Equals
(two));
// True
Console.
WriteLine
(one == two);
// True
When all the values are the same, the comparisons are correctly evaluated as
true
. If you didn’t
choose to overload the
==
and
!=
operators, then the last comparison of
one == two
would evaluate
as
false
. For more information, see
Overload ValueObject equality operators
.
How to persist value objects in the database with EF Core 2.0 and later
You just saw how to define a value object in your domain model. But how can you actually persist it
into the database using Entity Framework Core since it usually targets entities with identity?
Background and older approaches using EF Core 1.1
As background, a limitation when using EF Core 1.0 and 1.1 was that you could not use
complex types
as defined in EF 6.x in the traditional .NET Framework. Therefore, if using EF Core 1.0 or 1.1, you
needed to store your value object as an EF entity with an ID field. Then, so it looked more like a value
object with no identity, you could hide its ID so you make clear that the identity of a value object is
not important in the domain model. You could hide that ID by using the ID as a
shadow property
.
Since that configuration for hiding the ID in the model is set up in the EF infrastructure level, it would
be kind of transparent for your domain model.
In the initial version of eShopOnContainers (.NET Core 1.1), the hidden ID needed by EF Core
infrastructure was implemented in the following way in the DbContext level, using Fluent API at the
infrastructure project. Therefore, the ID was hidden from the domain model point of view, but still
present in the infrastructure.
// Old approach with EF Core 1.1
// Fluent API within the OrderingContext:DbContext in the Infrastructure project
void
ConfigureAddress
(EntityTypeBuilder addressConfiguration)
{
addressConfiguration.
ToTable
(
"address"
, DEFAULT_SCHEMA);
addressConfiguration.
Property
<
int
>(
"Id"
)
// Id is a shadow property
.
IsRequired
();
addressConfiguration.
HasKey
(
"Id"
);
// Id is a shadow property
}
218
CHAPTER 6 | Tackle Business Complexity in a Microservice with DDD and CQRS Patterns
However, the persistence of that value object into the database was performed like a regular entity in
a different table.
With EF Core 2.0 and later, there are new and better ways to persist value objects.
Dostları ilə paylaş: |