How You Can Design Domain-Driven Design Aggregates...

47
Vaughn Vernon vvernon@shiftmethod.com Copyright © 2012-2014 Vaughn Vernon. All rights reserved. How You Can Design Domain-Driven Design Aggregates Effectively Using .NET

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

Author Instructor

idddworkshop.com

@VaughnVernon

http://VaughnVernon.co

[email protected]

Overview

Common pitfalls of Aggregate design

Follow the Rules of Aggregate Design

Designing Aggregates Using Entity Framework and Azure Storage

Aggregate Parts

Common Pitfalls

Navigational convenience, but incorrect invariants

Common Pitfalls

Navigational convenience, but incorrect invariants

Pluses and Minuses

+: Navigational convenience; --: Performance, memory, txns fails

Incorrect Invariants

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

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;

...

}

Usage Scenario

Unrelated: Planning new backlog item & scheduling new release

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.

Rule: Design

Small AggregatesNot small.

Second Attempt

Reference Implications

public class BacklogItem : Entity

{

...

private Product product;

...

}

Rule: Reference By ID

public class BacklogItem : Entity

{

...

private ProductId productId;

...

}

Azure Table Storage

Pat Helland, Amazon.com: Life Beyond Distributed Transactions

1324fde 33de72f 85ea2b6 a75cb89

Continuous repartitioning1324fde

How To Update?

Rule: Use Eventual Consistency Outside

the Boundary

DDD p128: Any rule that spans AGGREGATES will not be expected to be up

Eventual Consistency

Effective Design?

Entity Framework (1)

Entity Framework (2)

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);

}

Azure Storage Table

Example uses Event Sourcing

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);

Azure Tables & Queues

Author Instructor

idddworkshop.com

47

@VaughnVernon

http://vaughnvernon.co/?page_id=168

http://VaughnVernon.co

[email protected]

Copyright © 2012-2014 Vaughn Vernon. All rights reserved.