Value Objects
Many attributes of an entity can be represented by primitive values such as integers or strings. However, it’s sometimes useful to represent an attribute by a type with more specific meaning. Such an attribute might be a single column, for example representing a Salary column by a Money type instead of Decimal, or it might be multiple columns, for example representing LocationX and LocationY columns by a Point or Position type instead of a two separate Doubles.
In these examples, Money, Point and Position are value objects. They are not entities, because they do not have identity of their own: they are just a way of representing entity attributes in a more business-meaningful way. Value objects are idiomatic in domain-driven design and are discussed in detail in Eric Evans’ book of the same name.
Defining a Value Object Type in the Designer
To define a value object type in the designer, drag a Value Object icon from the Toolbox into the model. You can name the type and add properties just as you do with entities.
Notice that a value object type does not have a base class or identity type, because a value object represents an attribute of an entity, and does not have identity in itself.
Creating a Value Object Member in the Designer
Once you have defined a value object type, you can create an entity property of that type by choosing the Value Object Property connector from the Toolbox and dragging a line from the entity to the value object type. You can edit the name of the property through the line label or the Name property.
Value Objects in Database-First Development
Relational databases don’t have a concept of value objects, so when you drag a table onto the designer, LightSpeed can’t automatically infer value objects from the table schema.
After dragging the table on, however, you may identify a set of columns which you want to represent as a value object member. If these columns’ names have a common prefix (see Value Object Database Mappings below) then you can extract them to a value object by selecting the columns, right-clicking the multiple selection and choosing Extract to Value Object. Depending on whether a value object type suitable for mapping these columns is already defined, the designer will offer to map the columns to an existing value object type, to add the columns to an existing value object type, or to create a new value object type (and a member of that type).
Defining a Value Object Type in Code
At the code level, a value object type is just an ordinary CLR type – class or struct. As with entities, LightSpeed is interested in the fields of the type, not the properties: therefore, you must not use automatic properties (because the backing field for an anonymous property does not have the ‘right’ name for LightSpeed to map it).
Value object types should be immutable. All fields should be read-only, and the wrapper properties should be get‑only.
A simple value object type |
public class Money |
Creating a Value Object Member in Code
Fields of value object type are defined in the same way as fields of primitive types, except that you must apply ValueObjectAttribute to the field.
A member of Money type |
public class Employee |
Setting Value Object Properties
Value objects are immutable. This means you cannot modify the properties of the value object directly.
employee.Salary.Amount = 50000; // Compiler error – Amount property is read-only |
The reason for this is that value objects represent values. Suppose you have an Employee entity with a Salary of $50000. If the Employee gets a raise, then their Salary changes to a new value of $60000. It would be wrong to think that $50000 has mutated into $60000. $60000 is a different value from $50000, so it must be a different instance. (Remember, value objects don’t have identity.)
Even if you created a mutable value object, you could not set properties this way, because value objects don’t have access to the Entity.Set method which is essential for notifying LightSpeed of changes needing saving – not to mention for UI interfaces such as IEditableObject and INotifyPropertyChanged.
So when you set a value object property, you must always set it to a new instance of the value type.
Setting value object properties |
employee.Salary = new Money(50000); |
A consequence of this is that when using data binding or data grids you must provide a way to edit your custom value types. In the Location example, the entity has a single Location, which will appear as a single column in a data grid. That column will need to display and allow editing of the Position value, but the editor will need to keep producing new Position objects. You cannot, for example, provide two text boxes, one bound to Location.X and one bound to Location.Y.
Value Object Database Mappings
By default, value object column names are prefixed with the name of the value object field on the containing class.
For example, suppose we have a Site entity with a Location property of type Position, and that Position has the properties X and Y. Then these would be mapped to columns named LocationX and LocationY in the Site database table.
You can map the column name suffixes associated with the fields in the value object in the same way as you map entity column names: by setting the Column Name option in the designer, or by applying ColumnAttribute in code.
For example, suppose that Position.X were mapped to Longitude and Position.Y to Latitude. Then the Site Location property would be mapped to the LocationLongitude and LocationLatitude columns in the database.
You can map the column name prefix associated with an occurrence of a value object by setting Column Name Prefix on the connector in the designer, or by applying ColumnAttribute to the value object reference in code.
For example, suppose that Site.Location were mapped to the Coordinates prefix. Then LightSpeed would map this to CoordinatesX and CoordinatesY columns in the database.
The following table shows how modifiers on a field in a value object (such as Position.X) and modifiers on an occurrence of a value object (such as Site.Location) affect the column name mapping.
Position.X | Site.Location | |
No modifier | Prefix “Coordinates” | |
No modifier | LocationX | CoordinatesX |
Column “Latitude” | LocationLatitude | CoordinatesLatitude |
In legacy databases, the columns of a value object may not share a common prefix, or there may not be a consistent set of suffixes across different sets of columns that you’d like to map to the same value object type. In this case, you can map individual fields of the value object at the occurrence level using ValueObjectColumnAttribute. At the time of writing, this is not supported in the designer and can only be used on hand-coded members.
Mapping value object columns to an irregular database schema |
public class Site : Entity<int> |