I am using domain driven design with an aggregate root and child entities.
The aggregate invariants are enforced via the aggregate root ConfigurableService
methods, chaining through to methods of the child entities, with a list of Groups
and Dependencies
which map requirements between the groups.
One invariant example is ConfigurableService
only allow the dependency to be made if the SkuIds are in contained within one of the Groups in the list.
If i go and make Groups or Dependencies public (as mongodb requires for persitence), then this domain logic can be bypassed - So i thought the class could be expanded with public properties which would call the entity methods, such as the below Dependencies
and Groups
properties:
public class ConfigurableService
{
List<Groups> groups = new List<Groups>();
Dependencies dependencies = new Dependencies();
public void AddDependency(SkuId on, SkuId requires)
{
if(IsContainsSku(on) && IsContainsSku(requires))
this.dependencies.SetRequiresFor(on, requires);
else
throw new Exception("SkuId doesnt exist");
}
public bool IsContainsSku(SkuId sku)
{
foreach(var group in groups)
{
if(group.ContainsSku(sku)==true)
{
return true;
}
}
return false;
}
// other code snipped for breverity
IEnumerable<Dependency> Dependencies
{
get { return this.dependencies.GetAllDependencies(); }
set
{
foreach(var dependency in value)
{
this.AddDependency(
new SkuId(dependency.Source),
new SkuId(dependency.Target)
);
}
}
}
IEnumerable<Group> Groups
{
get { return this.groups; }
set
{
this.groups.Clear();
foreach(var group in groups)
{
this.groups.Add(group);
}
}
}
}
I would repeat for each internal property that needs to be persisted as this will check the domain logic.
This would work fine during reading from repository when the object is built, as long as the properties are set in the correct order... for instance if Groups were set before Dependencies
we would end up throwing an exception (SkuID doesnt exist).
Questions
What are the recommended ways to protect invariants and allow Mongodb to persist/retrieve the domain entity object?
Can i control the order the properties are set when mongodb hydrates from its database?
Or would i be best to create a custom serialization method (how would i do this)?
My colleague came up with a custom serializer which resolved the issue, entity could be reconstructed manually from the BsonSerializer DTO:
public class ConfigurableServicePersistenceMapper
{
public ConfigurableServiceId Id { get; set; }
public string Description { get; set; }
public HashSet<Group> Groups { get; set; }
public Dependencies Dependencies { get; set; }
}
public class ConfigurableServiceSerializer : BsonBaseSerializer
{
public override void Serialize(BsonWriter bsonWriter, Type nominalType, object value, IBsonSerializationOptions options)
{
// implement using bsonWriter
if (nominalType != typeof(ConfigurableService))
throw new Exception("Object should be of type 'ConfigurableService'");
var obj = (ConfigurableService)value;
var map = new ConfigurableServicePersistenceMapper()
{
Dependencies = obj.Dependencies,
Description = obj.Description,
Groups = obj.Groups,
Id = obj.Id
};
BsonSerializer.Serialize(bsonWriter, map);
}
public override object Deserialize(BsonReader bsonReader, Type nominalType, Type actualType, IBsonSerializationOptions options)
{
// implement using bsonreader
if (nominalType != typeof(ConfigurableService))
throw new Exception("object should be of type 'ConfigurableService'");
var bson = BsonSerializer.Deserialize<BsonDocument>(bsonReader);
var configurableServiceMapper = BsonSerializer.Deserialize<ConfigurableServicePersistenceMapper>(bson);
var configurableService = new ConfigurableService(configurableServiceMapper.Id)
{
Description = configurableServiceMapper.Description
};
foreach (var group in configurableServiceMapper.Groups)
{
configurableService.NewGroup(group.Id);
var retrievedGroup = configurableService.GetGroup(group.Id);
retrievedGroup.Description = group.Description;
foreach (var sku in group.Skus)
{
retrievedGroup.Add(sku);
}
// set requirements
List<Group> groupList = new List<Group>(configurableServiceMapper.Groups);
foreach (var sku in group.Skus)
{
List<string> dependencies =
new List<string>(configurableServiceMapper.Dependencies.GetDependenciesFor(sku));
foreach (var dependencySkuString in dependencies)
{
retrievedGroup.SetRequirementFor(sku)
.Requires(new SkuId(dependencySkuString));
}
}
}
return configurableService;
}
}