A value object is a small object that represents a simple entity whose equality is not based on identity. Two value objects are equal when they have the same value, not necessarily being the same object
Main characteristics:
They have no identity.
They are immutable.
Value equality means that two variables of a
record
type are equal if the types match and all property and field values match. For other reference types, equality means identity. That is, two variables of a reference type are equal if they refer to the same object.
For
value objects
as
record
types, the synthesized equality members by default would be something like:
public
T1
P1
{
get
;
init
;
}
protected
virtual
Type
EqualityContract
=>
typeof
(
R1
);
public
override
bool
Equals
(
object
?
obj
)
=>
Equals
(
obj
as
R1
);
public
virtual
bool
Equals
(
R1
?
other
)
return
!(
other
is
null
)
&&
EqualityContract
==
other
.
EqualityContract
&&
EqualityComparer
<
T1
>.
Default
.
Equals
(
P1
,
other
.
P1
);
public
static
bool
operator
==(
R1
?
left
,
R1
?
right
)
=>
(
object
)
left
==
right
||
(
left
?.
Equals
(
right
)
??
false
);
public
static
bool
operator
!=(
R1
?
left
,
R1
?
right
)
=>
!(
left
==
right
);
public
override
int
GetHashCode
()
return
Combine
(
EqualityComparer
<
Type
>.
Default
.
GetHashCode
(
EqualityContract
),
EqualityComparer
<
T1
>.
Default
.
GetHashCode
(
P1
));
Even with some gaps between the canonical value object pattern in DDD and the owned entity type in EF Core,
it's currently the best way to persist value objects with EF Core
.
An owned entity type allows you to map types that do not have their own identity explicitly defined in the domain model and are used as properties, such as a
value object
.
The identity of instances of owned types is not completely their own. It consists of three components:
The identity of the owner
The navigation property pointing to them
In the case of collections of owned types, an independent component.
public
string
Name
{
get
;
}
public
int
Age
{
get
;
}
public
Address
Address
{
get
;
private
set
;
}
public
void
DefineAddress
(
Address
address
)
if
(
address
is
null
)
throw
new
BusinessException
(
"Home address must be informed"
);
// Benefit of the record Equality Contract
if
(
address
.
Equals
(
Address
))
return
;
Address
=
address
;
public
record
Address
:
Abstractions
.
ValueObject
// Empty constructor in this case is required by EF Core,
// because has a complex type as a parameter in the default constructor.
private
Address
()
{
}
public
Address
(
Street
street
,
string
zipCode
)
=>
(
Street
,
ZipCode
)
=
(
street
,
zipCode
);
public
Street
Street
{
get
;
private
init
;
}
public
string
ZipCode
{
get
;
private
init
;
}
public
class
PersonConfiguration
:
IEntityTypeConfiguration
<
Person
>
public
void
Configure
(
EntityTypeBuilder
<
Person
>
builder
)
builder
.
HasKey
(
user
=>
user
.
Id
);
// Configures a relationship where the Address is owned by (or part of) Person.
builder
.
OwnsOne
(
person
=>
person
.
Address
,
addressNavigationBuilder
=>
// Configures a different table that the entity type maps to when targeting a relational database.
addressNavigationBuilder
.
ToTable
(
"Addresses"
);
// Configures the relationship to the owner, and indicates the Foreign Key.
addressNavigationBuilder
.
WithOwner
()
.
HasForeignKey
(
"PersonId"
);
// Shadow Foreign Key
// Configure a property of the owned entity type, in this case the to be used as Primary Key
addressNavigationBuilder
.
Property
<
Guid
>(
"Id"
);
// Shadow property
// Sets the properties that make up the primary key for this owned entity type.
addressNavigationBuilder
.
HasKey
(
"Id"
);
// Shadow Primary Key
// Configures a relationship where the Street is owned by (or part of) Addresses.
// In this case, is not used "ToTable();" to maintain the owned and owner in the same table.
addressNavigationBuilder
.
OwnsOne
(
address
=>
address
.
Street
,
streetNavigationBuilder
=>
// Configures a relationship where the City is owned by (or part of) Street.
// In this case, is not used "ToTable();" to maintain the owned and owner in the same table.
streetNavigationBuilder
.
OwnsOne
(
street
=>
street
.
City
,
cityNavigationBuilder
=>
[
Id
]
uniqueidentifier
NOT
NULL
,
[
Name
]
varchar
(
128
)
NOT
NULL
,
[
Age
]
int
NOT
NULL
,
CONSTRAINT
[
PK_Persons
]
PRIMARY
KEY
([
Id
])
CREATE
TABLE
[
Addresses
]
[
Id
]
uniqueidentifier
NOT
NULL
,
[
Street_City_Name
]
varchar
(
128
)
NULL
,
[
Street_City_State_Country_Initials
]
varchar
(
8
)
NULL
,
[
Street_City_State_Country_Name
]
varchar
(
128
)
NULL
,
[
Street_City_State_Initials
]
varchar
(
8
)
NULL
,
[
Street_City_State_Name
]
varchar
(
128
)
NULL
,
[
Street_Name
]
varchar
(
128
)
NULL
,
[
Street_Number
]
int
NULL
,
[
ZipCode
]
varchar
(
32
)
NULL
,
[
PersonId
]
uniqueidentifier
NOT
NULL
,
CONSTRAINT
[
PK_Addresses
]
PRIMARY
KEY
([
Id
]),
CONSTRAINT
[
FK_Addresses_Persons_PersonId
]
FOREIGN
KEY
([
PersonId
])
REFERENCES
[
Persons
]
([
Id
])
ON
DELETE
CASCADE
CREATE
UNIQUE
INDEX
[
IX_Addresses_PersonId
]
ON
[
Addresses
]
([
PersonId
]);
The relationship between parent and child entities may be required or optional. A required relationship means that the child cannot exist without a parent, and if the parent is deleted or the relationship between the child and the parent is severed, then the child becomes orphaned. In this case, EF Core will perform a automatically child deletion.
I thought owned types were already implement by EFCore as fields of the owner.
Why are you overriding that and creating a separate table?
By default, you wouldn't need so much customization code.
For example, EFCore would have created the fields Person.Address_Street_City_Name, Person.Street_City_State_Country_Initials, Person.Street_Street_City_State_Country_Name, and so on, without requiring a join and a separate table, and foreign keys, etc.
That should be faster to query and btw, EFCore takes care of creating the shadow identities behind the scenes in this case, and you don't need to worry about them, because you don't need them, they're just there so that EFCore can keep track of the object to database mapping.
, thanks for the comment!
It's a trade-off between "JOIN" and "Let the table grow as the number of value objects grows".
In this case, I think those depend on the context.
Remember tables are for computers not humans.
You should do what is most performant not what is "easiest to read".
Your context hasn't been stated – but in general for databases optimize for the machine, not the human. That's what OO is for.
Built on
Forem
— the
open source
software that powers
DEV
and other inclusive communities.
Made with love and
Ruby on Rails
. DEV Community
©
2016 - 2024.