How You Can Design Domain-Driven Design Aggregates...
Transcript of How You Can Design Domain-Driven Design Aggregates...
Vaughn [email protected]
Copyright © 2012-2014 Vaughn Vernon. All rights reserved.
How You Can Design
Domain-Driven Design
Aggregates Effectively Using .NET
Overview
Common pitfalls of Aggregate design
Follow the Rules of Aggregate Design
Designing Aggregates Using Entity Framework and Azure Storage
Domain: Scrum
The domain is a Scrum-based project management application
Large-scale, multi-tenant, SaaS environment
Requires transactional consistency, performance, and scalability
Products, Backlog Items Releases, Sprints, Tasks...
Ubiquitous Language
Products have backlog items, releases, and sprints.
New product backlog items are planned.
New product releases are scheduled.
New product sprints are scheduled.
A planned backlog item may be scheduled for release.
A scheduled backlog item may be committed to a sprint.
The modeling began with these statements from Scrum...
(and “Tenant” from SaaS)
First Attempt: C#public class Product : Entity
{
private ISet<BacklogItem> backlogItems;
private string description;
private string name;
private ProductId productId;
private Set<Release> releases;
private Set<Sprint> sprints;
private TenantId tenantId;
...
}
Rule: Model True Invariants In Consistency Boundaries
c = a + b
Plug in some values to test the true invariant (a=2, b=3)
Aggregate BoundaryAggregateType1 {
int a;
int b;
int c;
operations...
}
A properly designed aggregate: can be modified in any
way required by the business with invariants completely
consistent within a single transaction.
Azure Table Storage
Pat Helland, Amazon.com: Life Beyond Distributed Transactions
1324fde 33de72f 85ea2b6 a75cb89
Continuous repartitioning1324fde
Rule: Use Eventual Consistency Outside
the Boundary
DDD p128: Any rule that spans AGGREGATES will not be expected to be up
public class Product
{
public Product(ProductId productId, ...)
{
State = new ProductState();
State.ProductKey = productId.Id;
...
}
internal Product(ProductState state) { State = state; }
public ProductId ProductId { get {
return new ProductId(State.ProductKey); } }
internal ProductState State { get; private set; }
public BacklogItem PlanBacklogItem(...) { ... }
...
}
EF Product
public class ProductState
{
[Key]
public string ProductKey { get; set; }
public ProductOwnerId ProductOwnerId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public List<ProductBacklogItemState> BacklogItems {get; set;}
public bool Equals(...) { ... }
public int GetHashCode() { ... }
public string ToString() { ... }
}
EF ProductState & [Key]
[ComplexType]
public class ProductId : Identity
{
public ProductId()
: base()
{
}
public ProductId(string id)
: base(id)
{
}
}
EF [ComplexType]
EF DbContext & Tablespublic class AgilePMContext : DbContext
{
public DbSet<ProductState> Products { get; set; }
public DbSet<ProductBacklogItemState> ProductBacklogItems
{ get; set; }
public DbSet<BacklogItemState> BacklogItems { get; set; }
public DbSet<TaskState> Tasks { get; set; }
}
EF Usageusing (var context = new AgilePMContext())
{
ProductRepository productRepository =
new EFProductRepository(context);
var product =
new Product(
new ProductId(),
new ProductOwnerId(),
"Test",
"A test product.");
productRepository.Add(product);
}
EF Repositorypublic interface ProductRepository
{
void Add(Product product);
Product ProductOfId(ProductId productId);
}
public class EFProductRepository : ProductRepository
{
private AgilePMContext context;
public EFProductRepository(AgilePMContext context)
{
this.context = context;
}
...
}
EF Repository Addpublic class EFProductRepository : ProductRepository
{
...
public void Add(Product product)
{
try
{
context.Products.Add(product.State);
}
catch (Exception e)
{
tracer.TraceInformation("Add() Unexpected: " + e);
}
}
...
}
EF Repository Findpublic Product ProductOfId(ProductId productId)
{
string key = productId.Id;
var state = default(ProductState);
try
{
state = (from p in context.Products
where p.ProductKey == key
select p).FirstOrDefault();
}
catch (Exception e)
{
tracer.TraceInformation("ProductOfId() Error: " + e);
}
...
return new Product(state);
}
public class Product : EventSourcedRootEntity
{
public Product(ProductId productId, ...)
{
Apply(new ProductCreated(productId.Id,
productOwnerId.Id, name, description));
}
internal Product(
IEnumerable<DomainEvent> eventStream,
int streamVersion)
: base(eventStream, streamVersion)
{
}
...
}
Azure ST Product
EventSourcedRootEntitypublic abstract class EventSourcedRootEntity
{
readonly List<DomainEvent> mutatingEvents;
readonly int unmutatedVersion;
...
protected void Apply(DomainEvent evt)
{
this.mutatingEvents.Add(evt);
Mutate(evt);
}
private void Mutate(DomainEvent evt)
{
((dynamic)this).When((dynamic)evt);
}
}
public class Product : EventSourcedRootEntity
{
internal void When(ProductCreated evt)
{
State = new ProductState();
State.ProductId = new ProductId(evt.ProductId);
State.ProductOwnerId =
new ProductOwnerId(evt.ProductOwnerId);
State.Name = evt.Name;
State.Description = evt.Description;
State.BacklogItems = new List<ProductBacklogItemState>();
}
...
}
Azure ST Product When
public class AzureProductRepository : ProductRepository
{
private static IDictionary<Type, XmlSerializer> serializers;
static AzureProductRepository()
{
serializers = new Dictionary<Type, XmlSerializer>();
serializers.Add(typeof(ProductCreated),
new XmlSerializer(typeof(ProductCreated)));
...
}
private CloudTable productsTable;
public AzureProductRepository(CloudTable productsTable)
{
this.productsTable = productsTable;
}
...
}
ST Repository
public class PersistentDomainEvent : TableEntity
{
private static UTF8Encoding encoding = new UTF8Encoding();
public PersistentDomainEvent(
XmlSerializer serializer,
string partitionName,
string aggregateId,
DomainEvent domainEvent,
int streamVersion)
{
this.PartitionKey = partitionName; // e.g. "Products";
this.RowKey = aggregateId + ":" + streamVersion;
this.DomainEvent = Serialize(serializer, domainEvent);
this.Type = domainEvent.GetType().ToString();
}
...
ST TableEntity
public void Save(Product product)
{
try
{
TableBatchOperation batchOperation = new ...
int currentStreamVersion = product.MutatedVersion;
var productId = product.ProductId;
foreach (DomainEvent domainEvent in product.MutatingEvents)
{
PersistentDomainEvent persistentEvent =
new PersistentDomainEvent(...);
batchOperation.Insert(persistentEvent);
}
productsTable.ExecuteBatch(batchOperation);
}
ST Repository Save
public Product ProductOfId(ProductId productId)
{
List<DomainEvent> eventStream = new List<DomainEvent>();
string productPrefixStreamId = productId.Id;
int streamVersion = 1;
for ( ; ; streamVersion++)
{
TableOperation retrieveOperation =
TableOperation.Retrieve<PersistentDomainEvent>(...);
TableResult retrieved = productsTable.Execute(...);
if (retrieved.Result != null)
{
DomainEvent devent = pevent.ToDomainEvent(serializer);
eventStream.Add(devent); }
else { break; }
}
return new Product(eventStream, streamVersion - 1);
ST Repository Find
Azure ST UsageProductRepository productRepository =
new AzureProductRepository(
tableClient.GetTableReference("Products"));
...
Product product =
new Product(new ProductId(), ...);
var bli1 = product.PlanBacklogItem(new BacklogItemId(), ...);
product.PlannedProductBacklogItem(bli1);
productRepository.Save(product);
var foundProduct =
productRepository.ProductOfId(product.ProductId);
47
@VaughnVernon
http://vaughnvernon.co/?page_id=168
http://VaughnVernon.co
Copyright © 2012-2014 Vaughn Vernon. All rights reserved.