Skip to content

Instantly share code, notes, and snippets.

@SteveBate
Created June 21, 2017 16:19
Show Gist options
  • Save SteveBate/5d8b611d1fdcbe7672a97b3a7e52576a to your computer and use it in GitHub Desktop.
Save SteveBate/5d8b611d1fdcbe7672a97b3a7e52576a to your computer and use it in GitHub Desktop.
Example of a Sale inside a POS system using event sourcing (Linqpad)
void Main()
{
var p1 = new StockItem { PartNo = "P100", Description = "Coca Cola", Qty = 1, Cost = 1.00m };
var p2 = new StockItem { PartNo = "P101", Description = "Pepsi Max", Qty = 2, Cost = 1.00m };
var p3 = new StockItem { PartNo = "P102", Description = "Pint San Miguel", Qty = 1, Cost = 3.50m };
var cfg = new SaleConfig { AddOnTax = false, TaxRate = 20.0m };
var sut = new Sale(Guid.NewGuid(), cfg);
sut.AddItem(p1);
sut.AddItem(p2);
sut.AddItem(p3);
var events = sut.GetUncommittedChanges();
var copy = new Sale(events);
copy.RemoveItem(p2);
copy.Discount(new Discount { Percent = 10, Reason = "Father's Day" });
copy.GetUncommittedChanges();
//copy.Discount(new Discount { Percent = 10, Reason = "Father's Day" });
}
public class AggregateRoot
{
public IEnumerable<Event> GetUncommittedChanges()
{
return _changes;
}
public void MarkAsCommitted()
{
_changes.Clear();
}
public void LoadHistoryFromEvents(IEnumerable<Event> events)
{
foreach (var e in events)
{
ApplyChange(e, false);
}
}
protected void ApplyChange(Event @event, bool isNew)
{
@event.Version = ++Version;
Action<Event> handler;
if (_handlers.TryGetValue(@event.GetType(), out handler))
handler(@event);
if (isNew) _changes.Add(@event);
}
protected void ApplyChange(Event @event)
{
ApplyChange(@event, true);
}
protected void Handles<T>(Action<T> handler) where T : Event
{
_handlers.Add(typeof(T), e => handler((T)e));
}
protected Guid Id { get; set; }
protected int Version { get; set; }
readonly Dictionary<Type, Action<Event>> _handlers = new Dictionary<Type, Action<Event>>();
readonly List<Event> _changes = new List<Event>();
}
public class Event
{
public Guid Id;
public int Version;
}
// domain
public class Sale : AggregateRoot
{
public Sale()
{
Handles<SaleWasCreated>(Apply);
Handles<StockItemWasAdded>(Apply);
Handles<StockItemWasRemoved>(Apply);
Handles<SaleWasDiscounted>(Apply);
Handles<TaxWasCalculated>(Apply);
}
public Sale(IEnumerable<Event> events) : this()
{
LoadHistoryFromEvents(events);
}
public Sale(Guid userId, SaleConfig config) : this()
{
ApplyChange(new SaleWasCreated
{
Id = Guid.NewGuid(),
UserId = userId,
Total = 0,
UsesAddOnTax = config.AddOnTax,
TaxRate = config.TaxRate
});
}
public void AddItem(StockItem item)
{
ApplyChange(new StockItemWasAdded {
Id = _id,
Part = item.PartNo,
Description = item.Description,
Qty = item.Qty,
Cost = item.Cost,
LineTotal = item.Qty * item.Cost,
Total = _currentTotal.Value + item.Qty * item.Cost
});
ApplyChange(new TaxWasCalculated
{
Id = _id,
Percent = _taxRate,
Amount = GetTax().Value,
AddOn = _addOnTax,
SubTotal = _addOnTax ? _currentTotal.Value : _currentTotal.Value - GetTax().Value,
Total = _addOnTax ? _currentTotal.Value + GetTax().Value : _currentTotal.Value
});
}
public void RemoveItem(StockItem item)
{
ApplyChange(new StockItemWasRemoved
{
Id = _id,
Part = item.PartNo,
Description = item.Description,
Qty = item.Qty,
Cost = item.Cost,
LineTotal = item.Qty * item.Cost,
Total = _currentTotal.Value - (item.Qty * item.Cost)
});
ApplyChange(new TaxWasCalculated
{
Id = _id,
Percent = _taxRate,
Amount = GetTax().Value,
AddOn = _addOnTax,
SubTotal = _addOnTax ? _currentTotal.Value : _currentTotal.Value - GetTax().Value,
Total = _addOnTax ? _currentTotal.Value + GetTax().Value : _currentTotal.Value
});
}
public void Discount(Discount discount)
{
if (_discounted) throw new InvalidOperationException("Discount cannot be applied more than once");
decimal disc = _currentTotal.Value / 100 * discount.Percent;
ApplyChange(new SaleWasDiscounted {
Id = _id,
Percent = discount.Percent,
Amount = Money.WithValue(disc).Value,
Reason = discount.Reason,
Total = _currentTotal.Value - Money.WithValue(disc).Value
});
ApplyChange(new TaxWasCalculated
{
Id = _id,
Percent = _taxRate,
Amount = GetTax().Value,
AddOn = _addOnTax,
SubTotal = _addOnTax ? _currentTotal.Value : _currentTotal.Value - GetTax().Value,
Total = _addOnTax ? _currentTotal.Value + GetTax().Value : _currentTotal.Value
});
}
void Apply(SaleWasCreated @event)
{
_id = @event.Id;
_currentTotal = Money.WithValue(@event.Total);
_addOnTax = @event.UsesAddOnTax;
_taxRate = @event.TaxRate;
}
void Apply(StockItemWasAdded @event)
{
_currentTotal += Money.WithValue(@event.LineTotal);
}
void Apply(StockItemWasRemoved @event)
{
_currentTotal -= Money.WithValue(@event.LineTotal);
}
void Apply(SaleWasDiscounted @event)
{
_discounted = true;
_currentTotal = Money.WithValue(@event.Total);
}
void Apply(TaxWasCalculated @event)
{
_currentTax = Money.WithValue(@event.Amount);
}
Money GetTax()
{
decimal amt = _addOnTax ? _currentTotal.Value * _taxRate / 100 : _currentTotal.Value * _taxRate / (100 + _taxRate);
return Money.WithValue(amt);
}
Guid _id;
bool _addOnTax;
bool _discounted;
decimal _taxRate;
Money _currentTotal = Money.Default;
Money _currentTax = Money.Default;
}
public class StockItem
{
public string PartNo { get; set; }
public string Description { get; set; }
public int Qty { get; set; }
public decimal Cost { get; set; }
}
// value objects
public class SaleConfig
{
public bool AddOnTax { get; set; }
public decimal TaxRate { get; set; }
}
public class Discount
{
public int Percent { get; set; }
public string Reason { get; set; }
public override bool Equals(object obj)
{
if (obj.GetType() != GetType()) return false;
var d = (Discount)obj;
return Percent.Equals(d.Percent) && Reason.Equals(d.Reason);
}
public override int GetHashCode()
{
int hash = 997;
hash = hash * 23 + Percent.GetHashCode();
hash = hash * 23 + Reason.GetHashCode();
return hash;
}
}
public class Money
{
public Money(decimal value)
{
Value = Math.Round(value, 2, MidpointRounding.AwayFromZero); ;
}
public decimal Value { get; private set; }
public virtual Money Negate()
{
return new Money(-Value);
}
// The addition (+), subtraction (-), multiplication (*) and division (/) operators
// are overloaded to allow us to provide a more natural means of calculating totals
// between two IMoney instances. For instance, Instead of having to add up the
// Value properties of two instances which are of type decimal, we can add now add
// up two IMoney instances.
public static Money operator +(Money left, Money right)
{
return new Money(left.Value + right.Value);
}
public static Money operator -(Money left, Money right)
{
return new Money(left.Value - right.Value);
}
public static Money operator *(Money left, Money right)
{
return new Money(left.Value * right.Value);
}
public static Money operator /(Money left, Money right)
{
return new Money(left.Value / right.Value);
}
public override bool Equals(object obj)
{
if (obj.GetType() != GetType()) return false;
var m = (Money)obj;
return Value.Equals(m.Value);
}
public override int GetHashCode()
{
int hash = 997;
hash = hash * 23 + Value.GetHashCode();
return hash;
}
public static Money Default
{
get { return new Money(0); }
}
public static Money WithValue(decimal value)
{
return new Money(value);
}
}
// events
public class SaleWasCreated : Event
{
public Guid UserId { get; set; }
public decimal Total { get; set; }
public bool UsesAddOnTax { get; set; }
public decimal TaxRate { get; set; }
}
public class StockItemWasAdded : Event
{
public string Part { get; set; }
public string Description { get; set; }
public int Qty { get; set; }
public decimal Cost { get; set; }
public decimal LineTotal { get; set; }
public decimal Total { get; set; }
}
public class StockItemWasRemoved : Event
{
public string Part { get; set; }
public string Description { get; set; }
public int Qty { get; set; }
public decimal Cost { get; set; }
public decimal LineTotal { get; set; }
public decimal Total { get; set; }
}
public class SaleWasDiscounted : Event
{
public int Percent { get; set; }
public decimal Amount { get; set; }
public decimal Total { get; set; }
public string Reason { get; set; }
}
public class TaxWasCalculated : Event
{
public decimal Percent { get; set; }
public decimal Amount { get; set; }
public bool AddOn { get; set; }
public decimal SubTotal { get; set; }
public decimal Total { get; set; }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment