How to deal with value objects in Entity Framework?
Asked Answered
F

3

23

How do I persist value objects in Entity Framework without polluting my domain model? EF (well, relational DBs in general) require me to define a key - which my value objects don't have out of the box, for example

public class Tag : ValueObject<Tag>
{
   private readonly string name;

   public Tag(string name)
   {
      this.name = name;
   }

   public string Name { get { return this.name; }}
}

On the other hand, I shouldn't address persistence concerns in the model. Am I really supposed to create another class that includes all the fields from the value object plus a key property and then map them to each other? I'd rather not.

Is there maybe a more elegant solution?

Frightened answered 23/5, 2015 at 3:6 Comment(2)
You could inherit the class and add key property to derived classSocialist
I thought of that, but I'd still end up with 2 classes per value object.Frightened
C
18

Vaughn Vernon writes about Persisting Value Objects (page 248) in his excellent book Implementing Domain-Driven Design.

ORM and Single Value Objects

The basic idea is to store each of the attributes of the Value in separate columns of the row where its parent Entity is stored. Said another way, a single Value Object is denormalized into its parent Entity's row. There are advantages to employing convention for column naming to clearly identity and standardize the way serialized objects are named.

ORM and Many Values Backed by a Database Entity

A very straightforward approach to persisting a collection of Value instances using an ORM and a relational database is to treat the Value type as an entity in the data model. (...) To accomplish this we can employ a Layer Supertype.

Sample bounded contexts in C# can be found here: https://github.com/VaughnVernon/IDDD_Samples_NET

Cappuccino answered 23/5, 2015 at 6:3 Comment(2)
Yeah, that's what I ended up doing. ValueObject<T> now has an integer key and so have all derived value objects. That will amount to n+1 classes instead of 2n classes. The ValueObject<T> class is part of the shared kernel, so it's not directly related to the model.Frightened
unless dealing with a 1-N (or many to many), use entity frameworks complex type.Halm
I
9

I'm currently working through some of these same challenges. I'm not really a fan of adding an Id to your base ValueObject<T> class since this gives an Id to all value objects whether or not they are needed plus as a value object by definition has no Id, it's anything inheriting that base type will no longer be a value object in the pure sense.

Before I go further, I would note that a key concept in coding DDD is that you don't have to be pure DDD everywhere, just so long as you know what concessions your making and their trade-offs. That being said, your approach can certainly be considered fine, however I believe it's adds a concession that may not really necessary. Primarily, this impacts the equality of your value objects. With the addition of Id, two Tags, even with the same name are no longer equal.

Here are my approaches to this situation: First the easy one, not really applicable to what I think your problem is but it's important. This is the single value object in the first part of Martin's answer.

  • Make the Value Object a Property of an Entity.

As long as your value object consists of just simple type properties, Entity Framework will map this just fine.

For Example:

    public class BlogEntry : Entity<Guid>
    {
         public String Text { get; private set; }
         public Tag Tag { get; private set; }

         // Constructors, Factories, Methods, etc
    }

Entity Framework will handle that just fine, what you will end up with is a single table BlogEntry that consists of simply of:

  • Id
  • Text
  • Tag_Name

Now I figure that's not really what your after in this case, but for many value objects it works out great. One I use frequently is a DateRange value object which consists of several properties. Then on my domain objects I simply have a property of the Type DateRange. EF maps those to the table for the domain object itself.

I bring this up because going back to the concession we made of adding Id to the ValueObject<T> base type, even though it Id may not be listed in your domain object concrete implementation, it is still there and will still get picked up by Entity Framework which for this, probably the most common value object use case no longer working quite as nicely.

OK, finally, onto your specific case (which I too have run into a few times). Here is how I have opted to handle the need for a entity to contain a list of Value Objects. Basically it boils down to expanding our understanding of the domain. Assuming the Tag value object is for recording Tag's in a blog post, the way I look at it is that a BlogPost contains a list of PostTag with the value of Tag. Yes it is one more class, but you don't need to add it for every value object, it's only needed when you have a list of value objects, and I think better expresses what is happening.

So here is an example of adding a list of a value object to an entity (using your value object of Tag above):

    public class BlogEntry : Entity<Guid>
    {
         public String Text { get; private set; }
         public ICollection<PostTag> PostTags { get; private set; }

         // Constructors:
         private BlogEntry(Guid id) : base(id) { }
         protected BlogEntry() : this(Guid.NewGuid()) { }

         // Factories:
         public static BlogEntry Create (String text, ICollection<PostTag> tags = null)
         {
             if(tags == null) { tags = new List<PostTag>(); }
             return new BlogEntry(){ Text = text, Tags = tags };
         }        

         // Methods:
         public void AddTag(String name)
         {
             PostTags.Add(PostTag.Create(name));
         }
    }

    public class PostTag : Entity<Guid>
    {
        // Properties:
        public Tag Tag { get; private set; }
        public DateTime DateAdded { get; private set; } // Properties that aren't relevant to the value of Tag.

        // Constructors:
        private PostTag(Guid id) : base(id) { }
        protected PostTag() : this(Guid.NewGuid()) { }

        // Factories:
        public static PostTag Create(Tag tag) 
        { 
            return new PostTag(){ Tag = tag, DateAdded = DateTime.Now };
        }

        public static PostTag Create(Tag tag, DateTime dateAdded) 
        { 
            return new PostTag(){ Tag = tag, DateAdded = dateAdded };
        }
    }

That will allow your BlogEntry to contain multiple tags without compromising value objects and Entity Framework will map it just fine without the need to do anything special.

Ichthyology answered 23/2, 2016 at 22:40 Comment(3)
Another technique I have been enjoying lately with this sort of thing is storing the collection of value objects as a Json string. Your entity object containing the collection can handle the parsing in and out of a string so that the rest of your model still works with it as a collection but for storage it goes in as a simple string. SQL Server can work with Json data as well if you need a manual query, although it's a bit more work. As a bonus if your views are using Ajax/Jquery you can just use the JsonString property as is.Ichthyology
Could I ask: 1- Why we just use Tag as Entity instead of VO because as u explained we had to create a useless entity as a wrapper to the real VO! 2-Can we add a foreign key -->the id of BlogEntry in PostTag? 3-Does EF handle this case by creating two tables 1-m ?Rendarender
If it were really as simple as a tag and that tag was only ever used for the post then yeah, it would probably be alright that way as well. However the idea here is that tag could be something that carries more complex logic and if you had a bunch of things that used tag you would have to duplicate that logic every time vs maintain a single tag VO.Ichthyology
S
1

I think it may be useful to know for those who use Entity Framework Core 2.0 that

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.

Here is detailed information from Microsoft about this feature:

Persist value objects as owned entity types in EF Core 2.0 and later 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 2.0 and later.

The owned entity type feature was added to EF Core since version 2.0.

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, within any of your entities. An owned entity type shares the same CLR type with another entity type (that is, it's just a regular class). The entity containing the defining navigation is the owner entity. When querying the owner, the owned types are included by default.

Just by looking at the domain model, an owned type looks like it doesn't have any identity. However, under the covers, owned types do have identity, but the owner navigation property is part of this identity.

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 (supported in EF Core 2.2 and later).

By convention, a shadow primary key is created for the owned type and it will be mapped to the same table as the owner by using table splitting. This allows to use owned types similarly to how complex types are used in EF6 in the traditional .NET Framework.

It is important to note that owned types are never discovered by convention in EF Core, so you have to declare them explicitly.

Additional details on owned entity types

-Owned types are defined when you configure a navigation property to a particular type using the OwnsOne fluent API.

-The definition of an owned type in our metadata model is a composite of: the owner type, the navigation property, and the CLR type of the owned type.

-The identity (key) of an owned type instance in our stack is a composite of the identity of the owner type and the definition of the owned type.

Owned entities capabilities

-Owned types can reference other entities, either owned (nested owned types) or non-owned (regular reference navigation properties to other entities).

-You can map the same CLR type as different owned types in the same owner entity through separate navigation properties.

-Table splitting is set up by convention, but you can opt out by mapping the owned type to a different table using ToTable.

-Eager loading is performed automatically on owned types, that is, there's no need to call .Include() on the query.

-Can be configured with attribute [Owned], using EF Core 2.1 and later.

-Can handle collections of owned types (using version 2.2 and later).

Owned entities limitations

-You can't create a DbSet of an owned type (by design).

-You can't call ModelBuilder.Entity() on owned types (currently by design).

-No support for optional (that is, nullable) owned types that are mapped with the owner in the same table (that is, using table splitting). This is because mapping is done for each property, we don't have a separate sentinel for the null complex value as a whole.

-No inheritance-mapping support for owned types, but you should be able to map two leaf types of the same inheritance hierarchies as different owned types. EF Core will not reason about the fact that they are part of the same hierarchy.

Main differences with EF6's complex types

-Table splitting is optional, that is, they can optionally be mapped to a separate table and still be owned types.

-They can reference other entities (that is, they can act as the dependent side on relationships to other non-owned types).

https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/implement-value-objects

Smocking answered 7/6, 2020 at 14:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.