This article reviews some of the different Identity Value Object Types and is an addition to Identity Value Objects. We’re going to go over abstract implementations for some common ones.
Note: If you’re not familiar with the basic concept of Value Objects go back and read Value Objects: The Basics. If you’re not yet familiar with Identity Value Objects already you can learn more about Identity Value Objects.
Let’s move right into the meat of this post.
GUID Identifiers
The first Identity Value Object type is a wrapped GUID
identifier.
// Represents a company ID using a globally unique identifier.
public class CompanyId : GuidIdentityValueObject
{
// Parameterless constructor required by ORM
private CompanyId() { }
public CompanyId(Guid companyId)
{
_setId(companyId);
}
}
The GuidIdentityValueObject
base class reduces duplicate code and can be seen below.
public abstract class GuidIdentityValueObject : ValueObjectBase where T : class
{
public Guid Identity { get; private set; }
protected void _setId(Guid id)
{
if (id == Guid.Empty)
{
throw new DomainException($"{nameof(T)} cannot be empty.");
}
Identity = id;
}
public override string ToString()
{
return Identity.ToString();
}
protected override IEnumerable<object> GetAtomicValues()
{
yield return Identity;
}
}
Note: ValueObjectBase
has been defined in a previous article Value Objects: The Basics.
Auto-Increment Identifiers
The second Identity Value Object type wraps an int
or long
.
// Represents a company ID using an int/long (e.g. auto-incremented) identifier.
public class CompanyId : LongIdentityValueObject
{
// Parameterless constructor required by ORM
private CompanyId() { }
public CompanyId(long companyId)
{
_setId(companyId);
}
}
The LongIdentityValueObject
base class can be seen below.
public abstract class LongIdentityValueObject : ValueObjectBase where T : class
{
public long Identity { get; private set; }
protected void _setId(long id)
{
if (id < 1)
{
throw new DomainException($"{nameof(T)} cannot be less than '1'.");
}
Identity = id;
}
public override string ToString()
{
return Identity.ToString();
}
protected override IEnumerable<object> GetAtomicValues()
{
yield return Identity;
}
}
String Identifiers
The third Identity type wraps a string.
// Represents a company ID using a string identifier.
public class CompanyId : StringIdentityValueObject
{
// Parameterless constructor required by ORM
private CompanyId() { }
public CompanyId(string companyId)
{
_setId(companyId);
}
}
The StringIdentityValueObject
base class can be seen below.
public abstract class StringIdentityValueObject : ValueObjectBase where T : class
{
public string Identity { get; private set; }
protected void _setId(string id)
{
if (string.IsNullOrWhitespace(id))
{
throw new DomainException($"{nameof(T)} cannot be empty.");
}
// Optionally, add string length validation
Identity = id;
}
public override string ToString()
{
return Identity;
}
protected override IEnumerable<object> GetAtomicValues()
{
yield return Identity;
}
}
Complex Identifiers
The final Identity Value Object type I’d like to cover is a complex identifier. Before diving into this one let’s understand the business requirements and model a little bit more.
This is for an eCommerce marketplace that allows a Company to create multiple Products (a Product
entity) to sell. The team (development, stake holders, etc) decide Products should have meaningful identifiers to lookup the Company
or Product
without first fetching the Product from the database.
After reviewing the requirements the team decides Product identifiers are constructed using the CompanyId
and a random string concatenated with a hyphen. The final ProductId
format is “<CompanyId>-<UniqueString>”.
// Represents a complex Product ID identifier.
public class ProductId : StringIdentityValueObject
{
private string _companyId;
public CompanyId CompanyId {
get =>
{
if (_companyId == null)
{
_companyId = Identity.Split('-')[0];
}
return new CompanyId(_companyId);
}
}
// Parameterless constructor required by ORM
private ProductId() { }
public ProductId(CompanyId companyId, string productId)
{
_setCompanyProductId(companyId, productId);
}
private void _setCompanyProductId(CompanyId companyId, string productId)
{
if (string.IsNullOrWhitespace(productId))
{
throw new DomainException($"Product ID cannot be empty.");
}
// Optionally, add string length validation
_setId($"{companyId}-{productId}");
}
}
As you can see the complex identity value object scenario is helpful for the software as well as many teams within your organization. You have less round trips to the database. It’s easier for support/development/debugging teams to identify the owning Company at a glance. Finally, the end result of this pattern and its benefits produce more meaningful domain identifiers.
However, think you should strive to use Identity Value Objects throughout your model, even though most of the time they will simply wrap a primitive type like Guid
, int
, string
, etc.