2

Aggregate (Root) Design: Behavior & Data

 3 years ago
source link: https://codeopinion.com/aggregate-root-design-behavior-data/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

How do you persist your Aggregate using Entity Framework? Using a Repository to get/set your Aggregate Root? I see a lot of examples that bend over backward to make their Entities expose behaviors while try to encapsulate data. It doesn’t need to be difficult. And if you’re not using an ORM, what’s a way that you can capture the changes made by the Aggregate Root so that you can persist them writing SQL?

YouTube

Check out my YouTube channel where I post all kinds of content that accompanies my posts including this video showing everything that is in this post.

Entity Framework

The most common examples I see are creating an Aggregate using Entity Framework entities. In the very simplest of use-cases, I think it can work. However, I’ve watched/read enough content that makes me shake my head about the hoops people will jump through to make their data persistence model also be their domain model. This is often because they are trying to hide the actual data or conform to how EF needs the entity to behave.

They don’t need to be the same object.

Your domain model is about exposing behaviors and encapsulating data.

If you’re not exposing data from an aggregate, meaning there’s no way for callers/consumers to get data from it (think CQRS), then you can encapsulate your data model inside your aggregate root. Your data model at that point is your Entity Framework entities. Your aggregate root is your aggregate.

Separate Aggregate & Data

This is very simple but allows you to use EF exactly how it’s intended and create an Aggregate Root that simply exposes behaviors and encapsulate your data model (EF).

First, let’s start with our Entity Framework Entities, which are basically now just our data model. They contain no behavior. Simply a data model used for persistence.

public class ShoppingCart { public ShoppingCart(Guid shoppingCartId, Guid customerId) { ShoppingCartId = shoppingCartId; CustomerId = customerId; }

public Guid ShoppingCartId { get; private set; } public Guid CustomerId { get; private set; } public IList<ShoppingCartItem> Items { get; set; } = new List<ShoppingCartItem>(); }

public class ShoppingCartItem { public ShoppingCartItem(Guid shoppingCartId, Guid productId, int quantity, decimal price) { ShoppingCartId = shoppingCartId; ProductId = productId; Quantity = quantity; Price = price; }

public Guid ShoppingCartId { get; set; } public Guid ProductId { get; private set; } public int Quantity { get; set; } public decimal Price { get; private set; } }

Next, all of our behavior goes into a separate class that takes the ShoppingCart as a parameter in the ctor. We’ll manipulate this data model in all of our behavior methods. We don’t ever expose the data model to any outside caller/consumer.

public class ShoppingCartDomain { private readonly ShoppingCart _shoppingCart;

public ShoppingCartDomain(ShoppingCart shoppingCart) { _shoppingCart = shoppingCart; }

public void AddItem(Guid productId, int quantity, decimal price) { var existingItem = _shoppingCart.Items.SingleOrDefault(x => x.ProductId == productId); if (existingItem != null) { existingItem.Quantity += quantity; } else { _shoppingCart.Items.Add(new ShoppingCartItem(_shoppingCart.ShoppingCartId, productId, quantity, price)); } }

public void RemoveItem(Guid productId) { var product = _shoppingCart.Items.SingleOrDefault(x => x.ProductId == productId); if (product != null) { _shoppingCart.Items.Remove(product); } } }

Finally, we use a repository to Get and Save a ShopingCartDomain.

There are a lot of opinions about Repositories. My opinion is they should be for constructing and saving your behavior only aggregate. This generally means you’ll only ever have 2 methods: Get() and Save()

The repository will get the data using Entity Framework, then construct a new instance of our Aggregate and pass in the data model.

public class ShoppingCartDomainRepository { private readonly SalesDbContext _dbContext;

public ShoppingCartDomainRepository(SalesDbContext dbContext) { _dbContext = dbContext; }

public async Task<ShoppingCartDomain> GetShoppingCart(Guid shoppingCartId) { var data = await _dbContext.ShoppingCarts .Include(x => x.Items) .SingleAsync(x => x.ShoppingCartId == shoppingCartId);

return new ShoppingCartDomain(data); }

public async Task Save() { await _dbContext.SaveChangesAsync(); } }

Aggregates (Root) without an ORM

If you’re not using an ORM like Entity Framework, but still want to create a domain with behavior then the ultimate problem is change tracking. With an ORM, it’s doing the change tracking of knowing which properties on your entities changed that it needs to persist to your database.

This is what needs to be implemented (change tracking) if you’re not using an ORM.

To do so, I like to implement this using events to represent state changes.

public class ShoppingCartEventDomain { private List<object> _events = new List<object>(); private readonly ShoppingCart _shoppingCart;

public ShoppingCartEventDomain(ShoppingCart shoppingCart) { _shoppingCart = shoppingCart; }

public List<object> GetEvents() { return _events; }

public void AddItem(Guid productId, int quantity, decimal price) { var existingItem = _shoppingCart.Items.SingleOrDefault(x => x.ProductId == productId); if (existingItem != null) { _events.Add(new QuantityIncremented { ShoppingCartId = _shoppingCart.ShoppingCartId, ProductId = productId, Quantity = quantity }); } else { _shoppingCart.Items.Add(new ShoppingCartItem(_shoppingCart.ShoppingCartId, productId));

_events.Add(new ItemAdded { ShoppingCartId = _shoppingCart.ShoppingCartId, ProductId = productId, Quantity = quantity, Price = price }); } }

public void RemoveItem(Guid productId) { var product = _shoppingCart.Items.SingleOrDefault(x => x.ProductId == productId); if (product != null) { _events.Add(new ItemRemoved { ShoppingCartId = _shoppingCart.ShoppingCartId, ProductId = productId, }); } } }

public class ItemAdded { public Guid ShoppingCartId { get; set; } public Guid ProductId { get; set; } public int Quantity { get; set; } public decimal Price { get; set; } }

public class ItemRemoved { public Guid ShoppingCartId { get; set; } public Guid ProductId { get; set; } }

public class QuantityIncremented { public Guid ShoppingCartId { get; set; } public Guid ProductId { get; set; } public int Quantity { get; set; } }

In our behavior methods, we’re recording and keeping track of them in the _events member. We’re also taking a parameter that represents the data model of the current state that our repository will build us.

When we want to save our state changes, our repository will iterate through the events, then have the appropriate SQL statements for them.

public class ShoppingCartRepository { private readonly IDbConnection _connection;

public ShoppingCartRepository(IDbConnection connection) { _connection = connection; }

public async Task<ShoppingCartEventDomain> GetShoppingCart(Guid shoppingCartId) { var shoppingCart = await _connection.QueryFirstAsync<ShoppingCart>("SELECT CustomerId FROM ShoppingCarts WHERE ShoppingCartId=@ShopingCartId", new {ShoppingCartId = shoppingCartId});

var items = await _connection.QueryAsync<ShoppingCartItem>("SELECT ProductId, Quantity FROM ShoppingCartItems WHERE ShoppingCartId=@ShopingCartId", new {ShoppingCartId = shoppingCartId}); shoppingCart.Items = items.ToList();

return new ShoppingCartEventDomain(shoppingCart); }

public async Task Save(ShoppingCartEventDomain shoppingCart) { var trx = _connection.BeginTransaction(); var evnts = shoppingCart.GetEvents(); foreach (var evnt in evnts) { if (evnt is ItemAdded itemAdded) { await ItemAdded(itemAdded, trx); } else if (evnt is QuantityIncremented quantityIncremented) { await QuantityIncremented(quantityIncremented, trx); } else if (evnt is ItemRemoved itemRemoved) { await ItemRemoved(itemRemoved, trx); } } trx.Commit(); }

private async Task ItemAdded(ItemAdded evnt, IDbTransaction trx) { await _connection.ExecuteAsync("INSERT INTO ShoppingCartItems (ShoppingCartId, ProductId, Quantity, Price) VALUES (@ShoppingCartId, @ProductId, @Quantity, @Price)", new { ShoppingCartId = evnt.ShoppingCartId, ProductID = evnt.ProductId, Quantity = evnt.Quantity, Price = evnt.Price }, trx); }

private async Task QuantityIncremented(QuantityIncremented evnt, IDbTransaction trx) { await _connection.ExecuteAsync("UPDATE ShoppingCartItems SET Quantity=Quantity+@Quantity WHERE ShoppingCartId=@ShoppingCartId AND ProductID=@ProductId", new { ShoppingCartId = evnt.ShoppingCartId, ProductID = evnt.ProductId, Quantity = evnt.Quantity, }, trx); }

private async Task ItemRemoved(ItemRemoved evnt, IDbTransaction trx) { await _connection.ExecuteAsync("DELETE FROM ShoppingCartItems WHERE ShoppingCartId=@ShoppingCartId AND ProductId=@ProductId", new { ShoppingCartId = evnt.ShoppingCartId, ProductID = evnt.ProductId }, trx); } }

Expose Behavior and Encapsulate Data

If you’re doing all sorts of odd things to make your ORM be a data model that also contains behaviors, just separate the two. If you’re not using an ORM, it doesn’t mean you can’t create aggregates. Use events as a method of change tracking and implement the SQL statements for each event that represents a state change.

Links

Clean up your Domain Model with Event Sourcing

Follow @CodeOpinion on Twitter

Leave this field empty if you're human:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK