This is a beginner’s tutorial on Value Object (VO) Pattern and Data Transfer Object (DTO) Patterns with examples. A Value Object is an object whose equality is based on value rather than identity. A Data Transfer Object is a data container for moving data.
1. Value Object Pattern - Definition
- Typically, when talking about “Value Object” (VO) in C#, we are thinking of a small object, whose main purpose is to hold data and has “value semantics”. That means that equality and assignment are based on value (in contrast to being based on identity/reference).
- The main idea behind Value Objects is to make objects compare on value rather than identity (references).
- Value Objects typically do not have any behavior except for storage, retrieval, equality comparison, and assignment.
- Value Objects typically live in the core of the application, participating significantly in the business logic.
- Value Objects are frequently made immutable.
- Related patterns: Immutable Object Pattern
2. C# Structs in VO Pattern
C# struct
s already have value semantics, but operators ==
and !=
need to be overridden, as well as the Hash
function. Here is an example of C# struct
s in VO pattern.
public struct CarStruct
{
public CarStruct(Char? brand, Char? model, int? year)
{
Brand = brand;
Model = model;
Year = year;
}
public Char? Brand { get; set; }
public Char? Model { get; set; }
public int? Year { get; set; }
public override string ToString()
{
return $"Brand:{Brand}, Model:{Model}, Year:{Year}";
}
public static bool operator ==(CarStruct? b1, CarStruct? b2)
{
if (b1 is null)
return b2 is null;
return b1.Equals(b2);
}
public static bool operator !=(CarStruct? b1, CarStruct? b2)
{
return !(b1 == b2);
}
public override int GetHashCode()
{
return (Brand, Model, Year).GetHashCode();
}
}
Console.WriteLine("-----");
Console.WriteLine("Assignment of Value Object - Struct");
CarStruct car7 = new CarStruct('T', 'C', 1991);
CarStruct car8 = car7;
Console.WriteLine($"Value object, car7={car7}");
Console.WriteLine($"Value object, car8={car8}");
string? address7 = Util.GetMemoryAddressOfStruct(ref car7);
string? address8 = Util.GetMemoryAddressOfStruct(ref car8);
Console.WriteLine($"Address car7={address7}, Address car8={address8}");
Console.WriteLine();
Console.WriteLine("Equality of Value Object - Struct");
CarStruct car5 = new CarStruct('T', 'C', 1991);
CarStruct car6 = new CarStruct('T', 'C', 1991);
Console.WriteLine($"Value object, car5={car5}");
Console.WriteLine($"Value object, car6={car6}");
bool equal56 = car5 == car6;
Console.WriteLine($"Value of car5==car6:{equal56}");
Console.WriteLine();
Console.ReadLine();
3. C# Class in VO Pattern
C# classes have reference semantics, so operators Equality, operators ==
and !=
need to be overridden, as well as the Hash
function. Problem is that the “assignment operator =” cannot be overloaded in C#, so we will create a copy-constructor instead. Here is an example of C# class in VO pattern:
public class CarClass
{
public CarClass(Char? brand, Char? model, int? year)
{
Brand = brand;
Model = model;
Year = year;
}
public CarClass(CarClass original)
{
Brand = original.Brand;
Model = original.Model;
Year = original.Year;
}
public Char? Brand { get; set; }
public Char? Model { get; set; }
public int? Year { get; set; }
public override string ToString()
{
return $"Brand:{Brand}, Model:{Model}, Year:{Year}";
}
public static bool operator ==(CarClass? b1, CarClass? b2)
{
if (b1 is null)
return b2 is null;
return b1.Equals(b2);
}
public static bool operator !=(CarClass? b1, CarClass? b2)
{
return !(b1 == b2);
}
public override bool Equals(object? obj)
{
if (obj == null)
return false;
return obj is CarClass b2 ? (Brand == b2.Brand &&
Model == b2.Model &&
Year == b2.Year) : false;
}
public override int GetHashCode()
{
return (Brand, Model, Year).GetHashCode();
}
}
Console.WriteLine("-----");
Console.WriteLine("Assignment of Value Object - Class");
CarClass car7 = new CarClass('T', 'C', 1991);
CarClass car8 = new CarClass(car7);
Console.WriteLine($"Value object, car7={car7}");
Console.WriteLine($"Value object, car8={car8}");
Tuple<string?, string?> addresses1 =Util.GetMemoryAddressOfClass(car7, car8);
Console.WriteLine($"Address car7={addresses1.Item1}, Address car8={addresses1.Item2}");
Console.WriteLine();
Console.WriteLine("Equality of Value Object - Class");
CarClass car5 = new CarClass('T', 'C', 1991);
CarClass car6 = new CarClass('T', 'C', 1991);
Console.WriteLine($"Value object, car5={car5}");
Console.WriteLine($"Value object, car6={car6}");
bool equal56 = car5 == car6;
Console.WriteLine($"Value of car5==car6:{equal56}");
Console.WriteLine();
Console.ReadLine();
4. Data Transfer Object Pattern - Definition
- Typically, when talking about “Data Transfer Object” (DTO) in C#, we are thinking of an object whose primary purpose is to act as a container for data that are to be transferred.
- The main idea behind Data Transfer Objects is to facilitate/simplify data transfer between layers/boundaries of the system. Typically, they achieve that by aggregating data that would be otherwise transferred in several calls.
- Data Transfer Objects typically do not have any behavior except for storage, retrieval, serialization, and deserialization.
- Data Transfer Objects typically live on the boundaries of the layers/system.
- Data Transfer Objects are usually not immutable since they do not benefit from immutability.
- Related patterns: Façade Pattern. That is because DTO frequently aggregates parts of several objects that need to be transferred.
5. C# Structs/Classes in DTO Pattern
Typically, communication between boundaries of layers/systems is a time-consuming operation. There, it is of interest to reduce the number of calls over boundaries or between layers. That is a motivation to use DTO object that aggregates data belonging to several objects, that would otherwise need to be transported in several calls. Here is one example of a DTO object.
public class Person
{
public Person(int id, String name, String nationality, int age)
{
Id =id;
Name =name;
Nationality =nationality;
Age =age;
}
public int Id { get; set; }
public String Name { get; set; }
public String Nationality { get; set;}
public int Age { get; set;}
}
public class Address
{
public Address(int id, String street, String city)
{
Id=id;
Street=street;
City=city;
}
public int Id { get; set; }
public string Street { get; set; }
public string City { get; set; }
}
public class Position
{
public Position(int id, String title, int salary)
{
Id=id; Title=title;Salary=salary;
}
public int Id { get; set; }
public String Title { get; set; }
public int Salary { get; set;}
}
public class EmployeeDto
{
public EmployeeDto()
{}
public EmployeeDto(Person person, Address address, Position position)
{
Name = person.Name;
City = address.City;
Title = position.Title;
}
public string? Name { get; set; }
public string? City { get; set; }
public string? Title { get; set; }
public override string ToString()
{
return $"Name:{Name}, City:{City}, Title:{Title}";
}
public byte[]? Serialize()
{
byte[]? result = null;
string json = JsonSerializer.Serialize(this);
result=System.Text.Encoding.Unicode.GetBytes(json);
return result;
}
public static EmployeeDto? Deserialize(byte[]? data)
{
EmployeeDto? result = null;
if (data != null)
{
string original2 = System.Text.Encoding.Unicode.GetString(data);
result = JsonSerializer.Deserialize<EmployeeDto>(original2);
}
return result;
}
}
Console.WriteLine("-----");
Person p = new Person(111, "Rafael", "Spanish", 36);
Address a = new Address(222, "Rafa's Way", "Majorca");
Position po = new Position(333, "Senior Programmer", 50_000);
EmployeeDto e1 = new EmployeeDto(p, a, po);
byte[]? emplyeeData1 = e1.Serialize();
EmployeeDto? e2 = EmployeeDto.Deserialize(emplyeeData1);
Console.WriteLine($"Original DTO, e1={e1}");
Console.WriteLine($"Received DTO, e2={e2?.ToString() ?? String.Empty}");
Console.WriteLine();
Console.ReadLine();
6. Utility for Finding Object Addresses
We developed a small utility that will give us the address of the objects in question, so by comparing addresses, it will be easily seen if we are talking about the same or different objects. The only problem is that our address-finding-utility has a limitation, that is, it works ONLY for objects on the heap that do not contain other objects on the heap (references). Therefore, we are forced to use only primitive values in our objects, and that is the reason why I needed to avoid using C# string
and am using only char
types.
public class Util
{
public static Tuple<string?, string?> GetMemoryAddressOfClass<T1, T2>(T1 o1, T2 o2)
where T1 : class
where T2 : class
{
string? address1 = null;
string? address2 = null;
GCHandle? handleO1 = null;
GCHandle? handleO2 = null;
if (o1 != null)
{
handleO1 = GCHandle.Alloc(o1, GCHandleType.Pinned);
}
if (o2 != null)
{
handleO2 = GCHandle.Alloc(o2, GCHandleType.Pinned);
}
if (handleO1 != null)
{
IntPtr pointer1 = handleO1.Value.AddrOfPinnedObject();
address1 = "0x" + pointer1.ToString("X");
}
if (handleO2 != null)
{
IntPtr pointer2 = handleO2.Value.AddrOfPinnedObject();
address2 = "0x" + pointer2.ToString("X");
}
if (handleO1 != null)
{
handleO1.Value.Free();
}
if (handleO2 != null)
{
handleO2.Value.Free();
}
Tuple<string?, string?> result =
new Tuple<string?, string?>(address1, address2);
return result;
}
public static unsafe string? GetMemoryAddressOfStruct<T1>(ref T1 o1)
where T1 : unmanaged
{
string? result = null;
fixed (void* pointer1 = (&o1))
{
result = $"0x{(long)pointer1:X}";
}
return result;
}
}
7. Conclusion
A Value Object (VO) is an object whose equality is based on value rather than identity. A Data Transfer Object (DTO) is a data container for moving data, whose purpose is to simplify data transfer between layers. These two pattern names are sometimes used interchangeably.
Value Object (VO) pattern and Data Transfer Object (DTO) pattern, although not very complicated, are frequently used and need to be in the repertoire of any serious C# programmer.
Related topics are “Immutable Object pattern” and “Records in C#”.
8. References
History
- 8th February, 2023: Initial version