Moved namespaces to ordering.domain project

This commit is contained in:
Sumit Ghosh 2021-10-08 17:54:02 +05:30
parent 46dad7ac75
commit 4758530f1f
23 changed files with 648 additions and 740 deletions

View File

@ -1,55 +1,48 @@
using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork; namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate;
using Ordering.Domain.Events;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate public class Buyer
: Entity, IAggregateRoot
{ {
public class Buyer public string IdentityGuid { get; private set; }
: Entity, IAggregateRoot
public string Name { get; private set; }
private List<PaymentMethod> _paymentMethods;
public IEnumerable<PaymentMethod> PaymentMethods => _paymentMethods.AsReadOnly();
protected Buyer()
{ {
public string IdentityGuid { get; private set; }
public string Name { get; private set; } _paymentMethods = new List<PaymentMethod>();
}
private List<PaymentMethod> _paymentMethods; public Buyer(string identity, string name) : this()
{
IdentityGuid = !string.IsNullOrWhiteSpace(identity) ? identity : throw new ArgumentNullException(nameof(identity));
Name = !string.IsNullOrWhiteSpace(name) ? name : throw new ArgumentNullException(nameof(name));
}
public IEnumerable<PaymentMethod> PaymentMethods => _paymentMethods.AsReadOnly(); public PaymentMethod VerifyOrAddPaymentMethod(
int cardTypeId, string alias, string cardNumber,
string securityNumber, string cardHolderName, DateTime expiration, int orderId)
{
var existingPayment = _paymentMethods
.SingleOrDefault(p => p.IsEqualTo(cardTypeId, cardNumber, expiration));
protected Buyer() if (existingPayment != null)
{ {
AddDomainEvent(new BuyerAndPaymentMethodVerifiedDomainEvent(this, existingPayment, orderId));
_paymentMethods = new List<PaymentMethod>(); return existingPayment;
} }
public Buyer(string identity, string name) : this() var payment = new PaymentMethod(cardTypeId, alias, cardNumber, securityNumber, cardHolderName, expiration);
{
IdentityGuid = !string.IsNullOrWhiteSpace(identity) ? identity : throw new ArgumentNullException(nameof(identity));
Name = !string.IsNullOrWhiteSpace(name) ? name : throw new ArgumentNullException(nameof(name));
}
public PaymentMethod VerifyOrAddPaymentMethod( _paymentMethods.Add(payment);
int cardTypeId, string alias, string cardNumber,
string securityNumber, string cardHolderName, DateTime expiration, int orderId)
{
var existingPayment = _paymentMethods
.SingleOrDefault(p => p.IsEqualTo(cardTypeId, cardNumber, expiration));
if (existingPayment != null) AddDomainEvent(new BuyerAndPaymentMethodVerifiedDomainEvent(this, payment, orderId));
{
AddDomainEvent(new BuyerAndPaymentMethodVerifiedDomainEvent(this, existingPayment, orderId));
return existingPayment; return payment;
}
var payment = new PaymentMethod(cardTypeId, alias, cardNumber, securityNumber, cardHolderName, expiration);
_paymentMethods.Add(payment);
AddDomainEvent(new BuyerAndPaymentMethodVerifiedDomainEvent(this, payment, orderId));
return payment;
}
} }
} }

View File

@ -1,21 +1,20 @@
using Microsoft.eShopOnContainers.Services.Ordering.Domain.SeedWork; using Microsoft.eShopOnContainers.Services.Ordering.Domain.SeedWork;
namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate;
{
/// <remarks>
/// Card type class should be marked as abstract with protected constructor to encapsulate known enum types
/// this is currently not possible as OrderingContextSeed uses this constructor to load cardTypes from csv file
/// </remarks>
public class CardType
: Enumeration
{
public static CardType Amex = new(1, nameof(Amex));
public static CardType Visa = new(2, nameof(Visa));
public static CardType MasterCard = new(3, nameof(MasterCard));
public CardType(int id, string name) /// <remarks>
: base(id, name) /// Card type class should be marked as abstract with protected constructor to encapsulate known enum types
{ /// this is currently not possible as OrderingContextSeed uses this constructor to load cardTypes from csv file
} /// </remarks>
public class CardType
: Enumeration
{
public static CardType Amex = new(1, nameof(Amex));
public static CardType Visa = new(2, nameof(Visa));
public static CardType MasterCard = new(3, nameof(MasterCard));
public CardType(int id, string name)
: base(id, name)
{
} }
} }

View File

@ -1,16 +1,13 @@
using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork; namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate;
using System.Threading.Tasks;
namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate //This is just the RepositoryContracts or Interface defined at the Domain Layer
//as requisite for the Buyer Aggregate
public interface IBuyerRepository : IRepository<Buyer>
{ {
//This is just the RepositoryContracts or Interface defined at the Domain Layer Buyer Add(Buyer buyer);
//as requisite for the Buyer Aggregate Buyer Update(Buyer buyer);
Task<Buyer> FindAsync(string BuyerIdentityGuid);
public interface IBuyerRepository : IRepository<Buyer> Task<Buyer> FindByIdAsync(string id);
{
Buyer Add(Buyer buyer);
Buyer Update(Buyer buyer);
Task<Buyer> FindAsync(string BuyerIdentityGuid);
Task<Buyer> FindByIdAsync(string id);
}
} }

View File

@ -1,46 +1,41 @@
using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork; namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate;
using Ordering.Domain.Exceptions;
using System;
namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate public class PaymentMethod
: Entity
{ {
public class PaymentMethod private string _alias;
: Entity private string _cardNumber;
private string _securityNumber;
private string _cardHolderName;
private DateTime _expiration;
private int _cardTypeId;
public CardType CardType { get; private set; }
protected PaymentMethod() { }
public PaymentMethod(int cardTypeId, string alias, string cardNumber, string securityNumber, string cardHolderName, DateTime expiration)
{ {
private string _alias;
private string _cardNumber;
private string _securityNumber;
private string _cardHolderName;
private DateTime _expiration;
private int _cardTypeId; _cardNumber = !string.IsNullOrWhiteSpace(cardNumber) ? cardNumber : throw new OrderingDomainException(nameof(cardNumber));
public CardType CardType { get; private set; } _securityNumber = !string.IsNullOrWhiteSpace(securityNumber) ? securityNumber : throw new OrderingDomainException(nameof(securityNumber));
_cardHolderName = !string.IsNullOrWhiteSpace(cardHolderName) ? cardHolderName : throw new OrderingDomainException(nameof(cardHolderName));
if (expiration < DateTime.UtcNow)
protected PaymentMethod() { }
public PaymentMethod(int cardTypeId, string alias, string cardNumber, string securityNumber, string cardHolderName, DateTime expiration)
{ {
throw new OrderingDomainException(nameof(expiration));
_cardNumber = !string.IsNullOrWhiteSpace(cardNumber) ? cardNumber : throw new OrderingDomainException(nameof(cardNumber));
_securityNumber = !string.IsNullOrWhiteSpace(securityNumber) ? securityNumber : throw new OrderingDomainException(nameof(securityNumber));
_cardHolderName = !string.IsNullOrWhiteSpace(cardHolderName) ? cardHolderName : throw new OrderingDomainException(nameof(cardHolderName));
if (expiration < DateTime.UtcNow)
{
throw new OrderingDomainException(nameof(expiration));
}
_alias = alias;
_expiration = expiration;
_cardTypeId = cardTypeId;
} }
public bool IsEqualTo(int cardTypeId, string cardNumber, DateTime expiration) _alias = alias;
{ _expiration = expiration;
return _cardTypeId == cardTypeId _cardTypeId = cardTypeId;
&& _cardNumber == cardNumber }
&& _expiration == expiration;
} public bool IsEqualTo(int cardTypeId, string cardNumber, DateTime expiration)
{
return _cardTypeId == cardTypeId
&& _cardNumber == cardNumber
&& _expiration == expiration;
} }
} }

View File

@ -1,36 +1,33 @@
using Microsoft.eShopOnContainers.Services.Ordering.Domain.SeedWork; using Microsoft.eShopOnContainers.Services.Ordering.Domain.SeedWork;
using System;
using System.Collections.Generic;
namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
public class Address : ValueObject
{ {
public class Address : ValueObject public String Street { get; private set; }
public String City { get; private set; }
public String State { get; private set; }
public String Country { get; private set; }
public String ZipCode { get; private set; }
public Address() { }
public Address(string street, string city, string state, string country, string zipcode)
{ {
public String Street { get; private set; } Street = street;
public String City { get; private set; } City = city;
public String State { get; private set; } State = state;
public String Country { get; private set; } Country = country;
public String ZipCode { get; private set; } ZipCode = zipcode;
}
public Address() { } protected override IEnumerable<object> GetEqualityComponents()
{
public Address(string street, string city, string state, string country, string zipcode) // Using a yield return statement to return each element one at a time
{ yield return Street;
Street = street; yield return City;
City = city; yield return State;
State = state; yield return Country;
Country = country; yield return ZipCode;
ZipCode = zipcode;
}
protected override IEnumerable<object> GetEqualityComponents()
{
// Using a yield return statement to return each element one at a time
yield return Street;
yield return City;
yield return State;
yield return Country;
yield return ZipCode;
}
} }
} }

View File

@ -1,17 +1,13 @@
using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork; namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
using System.Threading.Tasks;
namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate //This is just the RepositoryContracts or Interface defined at the Domain Layer
//as requisite for the Order Aggregate
public interface IOrderRepository : IRepository<Order>
{ {
//This is just the RepositoryContracts or Interface defined at the Domain Layer Order Add(Order order);
//as requisite for the Order Aggregate
public interface IOrderRepository : IRepository<Order> void Update(Order order);
{
Order Add(Order order);
void Update(Order order); Task<Order> GetAsync(int orderId);
Task<Order> GetAsync(int orderId);
}
} }

View File

@ -1,202 +1,195 @@
using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork; using Microsoft.eShopOnContainers.Services.Ordering.Ordering.Domain.Events;
using Ordering.Domain.Events;
using Ordering.Domain.Exceptions;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
public class Order
: Entity, IAggregateRoot
{ {
public class Order // DDD Patterns comment
: Entity, IAggregateRoot // Using private fields, allowed since EF Core 1.1, is a much better encapsulation
// aligned with DDD Aggregates and Domain Entities (Instead of properties and property collections)
private DateTime _orderDate;
// Address is a Value Object pattern example persisted as EF Core 2.0 owned entity
public Address Address { get; private set; }
public int? GetBuyerId => _buyerId;
private int? _buyerId;
public OrderStatus OrderStatus { get; private set; }
private int _orderStatusId;
private string _description;
// Draft orders have this set to true. Currently we don't check anywhere the draft status of an Order, but we could do it if needed
private bool _isDraft;
// DDD Patterns comment
// Using a private collection field, better for DDD Aggregate's encapsulation
// so OrderItems cannot be added from "outside the AggregateRoot" directly to the collection,
// but only through the method OrderAggrergateRoot.AddOrderItem() which includes behaviour.
private readonly List<OrderItem> _orderItems;
public IReadOnlyCollection<OrderItem> OrderItems => _orderItems;
private int? _paymentMethodId;
public static Order NewDraft()
{ {
// DDD Patterns comment var order = new Order();
// Using private fields, allowed since EF Core 1.1, is a much better encapsulation order._isDraft = true;
// aligned with DDD Aggregates and Domain Entities (Instead of properties and property collections) return order;
private DateTime _orderDate; }
// Address is a Value Object pattern example persisted as EF Core 2.0 owned entity protected Order()
public Address Address { get; private set; } {
_orderItems = new List<OrderItem>();
_isDraft = false;
}
public int? GetBuyerId => _buyerId; public Order(string userId, string userName, Address address, int cardTypeId, string cardNumber, string cardSecurityNumber,
private int? _buyerId; string cardHolderName, DateTime cardExpiration, int? buyerId = null, int? paymentMethodId = null) : this()
{
_buyerId = buyerId;
_paymentMethodId = paymentMethodId;
_orderStatusId = OrderStatus.Submitted.Id;
_orderDate = DateTime.UtcNow;
Address = address;
public OrderStatus OrderStatus { get; private set; } // Add the OrderStarterDomainEvent to the domain events collection
private int _orderStatusId; // to be raised/dispatched when comitting changes into the Database [ After DbContext.SaveChanges() ]
AddOrderStartedDomainEvent(userId, userName, cardTypeId, cardNumber,
cardSecurityNumber, cardHolderName, cardExpiration);
}
private string _description; // DDD Patterns comment
// This Order AggregateRoot's method "AddOrderitem()" should be the only way to add Items to the Order,
// so any behavior (discounts, etc.) and validations are controlled by the AggregateRoot
// in order to maintain consistency between the whole Aggregate.
public void AddOrderItem(int productId, string productName, decimal unitPrice, decimal discount, string pictureUrl, int units = 1)
{
var existingOrderForProduct = _orderItems.Where(o => o.ProductId == productId)
.SingleOrDefault();
if (existingOrderForProduct != null)
// Draft orders have this set to true. Currently we don't check anywhere the draft status of an Order, but we could do it if needed
private bool _isDraft;
// DDD Patterns comment
// Using a private collection field, better for DDD Aggregate's encapsulation
// so OrderItems cannot be added from "outside the AggregateRoot" directly to the collection,
// but only through the method OrderAggrergateRoot.AddOrderItem() which includes behaviour.
private readonly List<OrderItem> _orderItems;
public IReadOnlyCollection<OrderItem> OrderItems => _orderItems;
private int? _paymentMethodId;
public static Order NewDraft()
{ {
var order = new Order(); //if previous line exist modify it with higher discount and units..
order._isDraft = true;
return order;
}
protected Order() if (discount > existingOrderForProduct.GetCurrentDiscount())
{
_orderItems = new List<OrderItem>();
_isDraft = false;
}
public Order(string userId, string userName, Address address, int cardTypeId, string cardNumber, string cardSecurityNumber,
string cardHolderName, DateTime cardExpiration, int? buyerId = null, int? paymentMethodId = null) : this()
{
_buyerId = buyerId;
_paymentMethodId = paymentMethodId;
_orderStatusId = OrderStatus.Submitted.Id;
_orderDate = DateTime.UtcNow;
Address = address;
// Add the OrderStarterDomainEvent to the domain events collection
// to be raised/dispatched when comitting changes into the Database [ After DbContext.SaveChanges() ]
AddOrderStartedDomainEvent(userId, userName, cardTypeId, cardNumber,
cardSecurityNumber, cardHolderName, cardExpiration);
}
// DDD Patterns comment
// This Order AggregateRoot's method "AddOrderitem()" should be the only way to add Items to the Order,
// so any behavior (discounts, etc.) and validations are controlled by the AggregateRoot
// in order to maintain consistency between the whole Aggregate.
public void AddOrderItem(int productId, string productName, decimal unitPrice, decimal discount, string pictureUrl, int units = 1)
{
var existingOrderForProduct = _orderItems.Where(o => o.ProductId == productId)
.SingleOrDefault();
if (existingOrderForProduct != null)
{ {
//if previous line exist modify it with higher discount and units.. existingOrderForProduct.SetNewDiscount(discount);
if (discount > existingOrderForProduct.GetCurrentDiscount())
{
existingOrderForProduct.SetNewDiscount(discount);
}
existingOrderForProduct.AddUnits(units);
}
else
{
//add validated new order item
var orderItem = new OrderItem(productId, productName, unitPrice, discount, pictureUrl, units);
_orderItems.Add(orderItem);
}
}
public void SetPaymentId(int id)
{
_paymentMethodId = id;
}
public void SetBuyerId(int id)
{
_buyerId = id;
}
public void SetAwaitingValidationStatus()
{
if (_orderStatusId == OrderStatus.Submitted.Id)
{
AddDomainEvent(new OrderStatusChangedToAwaitingValidationDomainEvent(Id, _orderItems));
_orderStatusId = OrderStatus.AwaitingValidation.Id;
}
}
public void SetStockConfirmedStatus()
{
if (_orderStatusId == OrderStatus.AwaitingValidation.Id)
{
AddDomainEvent(new OrderStatusChangedToStockConfirmedDomainEvent(Id));
_orderStatusId = OrderStatus.StockConfirmed.Id;
_description = "All the items were confirmed with available stock.";
}
}
public void SetPaidStatus()
{
if (_orderStatusId == OrderStatus.StockConfirmed.Id)
{
AddDomainEvent(new OrderStatusChangedToPaidDomainEvent(Id, OrderItems));
_orderStatusId = OrderStatus.Paid.Id;
_description = "The payment was performed at a simulated \"American Bank checking bank account ending on XX35071\"";
}
}
public void SetShippedStatus()
{
if (_orderStatusId != OrderStatus.Paid.Id)
{
StatusChangeException(OrderStatus.Shipped);
} }
_orderStatusId = OrderStatus.Shipped.Id; existingOrderForProduct.AddUnits(units);
_description = "The order was shipped.";
AddDomainEvent(new OrderShippedDomainEvent(this));
} }
else
public void SetCancelledStatus()
{ {
if (_orderStatusId == OrderStatus.Paid.Id || //add validated new order item
_orderStatusId == OrderStatus.Shipped.Id)
{
StatusChangeException(OrderStatus.Cancelled);
}
_orderStatusId = OrderStatus.Cancelled.Id; var orderItem = new OrderItem(productId, productName, unitPrice, discount, pictureUrl, units);
_description = $"The order was cancelled."; _orderItems.Add(orderItem);
AddDomainEvent(new OrderCancelledDomainEvent(this));
}
public void SetCancelledStatusWhenStockIsRejected(IEnumerable<int> orderStockRejectedItems)
{
if (_orderStatusId == OrderStatus.AwaitingValidation.Id)
{
_orderStatusId = OrderStatus.Cancelled.Id;
var itemsStockRejectedProductNames = OrderItems
.Where(c => orderStockRejectedItems.Contains(c.ProductId))
.Select(c => c.GetOrderItemProductName());
var itemsStockRejectedDescription = string.Join(", ", itemsStockRejectedProductNames);
_description = $"The product items don't have stock: ({itemsStockRejectedDescription}).";
}
}
private void AddOrderStartedDomainEvent(string userId, string userName, int cardTypeId, string cardNumber,
string cardSecurityNumber, string cardHolderName, DateTime cardExpiration)
{
var orderStartedDomainEvent = new OrderStartedDomainEvent(this, userId, userName, cardTypeId,
cardNumber, cardSecurityNumber,
cardHolderName, cardExpiration);
this.AddDomainEvent(orderStartedDomainEvent);
}
private void StatusChangeException(OrderStatus orderStatusToChange)
{
throw new OrderingDomainException($"Is not possible to change the order status from {OrderStatus.Name} to {orderStatusToChange.Name}.");
}
public decimal GetTotal()
{
return _orderItems.Sum(o => o.GetUnits() * o.GetUnitPrice());
} }
} }
}
public void SetPaymentId(int id)
{
_paymentMethodId = id;
}
public void SetBuyerId(int id)
{
_buyerId = id;
}
public void SetAwaitingValidationStatus()
{
if (_orderStatusId == OrderStatus.Submitted.Id)
{
AddDomainEvent(new OrderStatusChangedToAwaitingValidationDomainEvent(Id, _orderItems));
_orderStatusId = OrderStatus.AwaitingValidation.Id;
}
}
public void SetStockConfirmedStatus()
{
if (_orderStatusId == OrderStatus.AwaitingValidation.Id)
{
AddDomainEvent(new OrderStatusChangedToStockConfirmedDomainEvent(Id));
_orderStatusId = OrderStatus.StockConfirmed.Id;
_description = "All the items were confirmed with available stock.";
}
}
public void SetPaidStatus()
{
if (_orderStatusId == OrderStatus.StockConfirmed.Id)
{
AddDomainEvent(new OrderStatusChangedToPaidDomainEvent(Id, OrderItems));
_orderStatusId = OrderStatus.Paid.Id;
_description = "The payment was performed at a simulated \"American Bank checking bank account ending on XX35071\"";
}
}
public void SetShippedStatus()
{
if (_orderStatusId != OrderStatus.Paid.Id)
{
StatusChangeException(OrderStatus.Shipped);
}
_orderStatusId = OrderStatus.Shipped.Id;
_description = "The order was shipped.";
AddDomainEvent(new OrderShippedDomainEvent(this));
}
public void SetCancelledStatus()
{
if (_orderStatusId == OrderStatus.Paid.Id ||
_orderStatusId == OrderStatus.Shipped.Id)
{
StatusChangeException(OrderStatus.Cancelled);
}
_orderStatusId = OrderStatus.Cancelled.Id;
_description = $"The order was cancelled.";
AddDomainEvent(new OrderCancelledDomainEvent(this));
}
public void SetCancelledStatusWhenStockIsRejected(IEnumerable<int> orderStockRejectedItems)
{
if (_orderStatusId == OrderStatus.AwaitingValidation.Id)
{
_orderStatusId = OrderStatus.Cancelled.Id;
var itemsStockRejectedProductNames = OrderItems
.Where(c => orderStockRejectedItems.Contains(c.ProductId))
.Select(c => c.GetOrderItemProductName());
var itemsStockRejectedDescription = string.Join(", ", itemsStockRejectedProductNames);
_description = $"The product items don't have stock: ({itemsStockRejectedDescription}).";
}
}
private void AddOrderStartedDomainEvent(string userId, string userName, int cardTypeId, string cardNumber,
string cardSecurityNumber, string cardHolderName, DateTime cardExpiration)
{
var orderStartedDomainEvent = new OrderStartedDomainEvent(this, userId, userName, cardTypeId,
cardNumber, cardSecurityNumber,
cardHolderName, cardExpiration);
this.AddDomainEvent(orderStartedDomainEvent);
}
private void StatusChangeException(OrderStatus orderStatusToChange)
{
throw new OrderingDomainException($"Is not possible to change the order status from {OrderStatus.Name} to {orderStatusToChange.Name}.");
}
public decimal GetTotal()
{
return _orderItems.Sum(o => o.GetUnits() * o.GetUnitPrice());
}
}

View File

@ -1,82 +1,78 @@
using Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork; namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
using Ordering.Domain.Exceptions;
namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate public class OrderItem
: Entity
{ {
public class OrderItem // DDD Patterns comment
: Entity // Using private fields, allowed since EF Core 1.1, is a much better encapsulation
// aligned with DDD Aggregates and Domain Entities (Instead of properties and property collections)
private string _productName;
private string _pictureUrl;
private decimal _unitPrice;
private decimal _discount;
private int _units;
public int ProductId { get; private set; }
protected OrderItem() { }
public OrderItem(int productId, string productName, decimal unitPrice, decimal discount, string PictureUrl, int units = 1)
{ {
// DDD Patterns comment if (units <= 0)
// Using private fields, allowed since EF Core 1.1, is a much better encapsulation
// aligned with DDD Aggregates and Domain Entities (Instead of properties and property collections)
private string _productName;
private string _pictureUrl;
private decimal _unitPrice;
private decimal _discount;
private int _units;
public int ProductId { get; private set; }
protected OrderItem() { }
public OrderItem(int productId, string productName, decimal unitPrice, decimal discount, string PictureUrl, int units = 1)
{ {
if (units <= 0) throw new OrderingDomainException("Invalid number of units");
{
throw new OrderingDomainException("Invalid number of units");
}
if ((unitPrice * units) < discount)
{
throw new OrderingDomainException("The total of order item is lower than applied discount");
}
ProductId = productId;
_productName = productName;
_unitPrice = unitPrice;
_discount = discount;
_units = units;
_pictureUrl = PictureUrl;
} }
public string GetPictureUri() => _pictureUrl; if ((unitPrice * units) < discount)
public decimal GetCurrentDiscount()
{ {
return _discount; throw new OrderingDomainException("The total of order item is lower than applied discount");
} }
public int GetUnits() ProductId = productId;
_productName = productName;
_unitPrice = unitPrice;
_discount = discount;
_units = units;
_pictureUrl = PictureUrl;
}
public string GetPictureUri() => _pictureUrl;
public decimal GetCurrentDiscount()
{
return _discount;
}
public int GetUnits()
{
return _units;
}
public decimal GetUnitPrice()
{
return _unitPrice;
}
public string GetOrderItemProductName() => _productName;
public void SetNewDiscount(decimal discount)
{
if (discount < 0)
{ {
return _units; throw new OrderingDomainException("Discount is not valid");
} }
public decimal GetUnitPrice() _discount = discount;
}
public void AddUnits(int units)
{
if (units < 0)
{ {
return _unitPrice; throw new OrderingDomainException("Invalid units");
} }
public string GetOrderItemProductName() => _productName; _units += units;
public void SetNewDiscount(decimal discount)
{
if (discount < 0)
{
throw new OrderingDomainException("Discount is not valid");
}
_discount = discount;
}
public void AddUnits(int units)
{
if (units < 0)
{
throw new OrderingDomainException("Invalid units");
}
_units += units;
}
} }
} }

View File

@ -1,52 +1,47 @@
namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate using Microsoft.eShopOnContainers.Services.Ordering.Domain.SeedWork;
namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
public class OrderStatus
: Enumeration
{ {
using global::Ordering.Domain.Exceptions; public static OrderStatus Submitted = new OrderStatus(1, nameof(Submitted).ToLowerInvariant());
using Microsoft.eShopOnContainers.Services.Ordering.Domain.SeedWork; public static OrderStatus AwaitingValidation = new OrderStatus(2, nameof(AwaitingValidation).ToLowerInvariant());
using System; public static OrderStatus StockConfirmed = new OrderStatus(3, nameof(StockConfirmed).ToLowerInvariant());
using System.Collections.Generic; public static OrderStatus Paid = new OrderStatus(4, nameof(Paid).ToLowerInvariant());
using System.Linq; public static OrderStatus Shipped = new OrderStatus(5, nameof(Shipped).ToLowerInvariant());
public static OrderStatus Cancelled = new OrderStatus(6, nameof(Cancelled).ToLowerInvariant());
public class OrderStatus public OrderStatus(int id, string name)
: Enumeration : base(id, name)
{ {
public static OrderStatus Submitted = new OrderStatus(1, nameof(Submitted).ToLowerInvariant()); }
public static OrderStatus AwaitingValidation = new OrderStatus(2, nameof(AwaitingValidation).ToLowerInvariant());
public static OrderStatus StockConfirmed = new OrderStatus(3, nameof(StockConfirmed).ToLowerInvariant());
public static OrderStatus Paid = new OrderStatus(4, nameof(Paid).ToLowerInvariant());
public static OrderStatus Shipped = new OrderStatus(5, nameof(Shipped).ToLowerInvariant());
public static OrderStatus Cancelled = new OrderStatus(6, nameof(Cancelled).ToLowerInvariant());
public OrderStatus(int id, string name) public static IEnumerable<OrderStatus> List() =>
: base(id, name) new[] { Submitted, AwaitingValidation, StockConfirmed, Paid, Shipped, Cancelled };
public static OrderStatus FromName(string name)
{
var state = List()
.SingleOrDefault(s => String.Equals(s.Name, name, StringComparison.CurrentCultureIgnoreCase));
if (state == null)
{ {
throw new OrderingDomainException($"Possible values for OrderStatus: {String.Join(",", List().Select(s => s.Name))}");
} }
public static IEnumerable<OrderStatus> List() => return state;
new[] { Submitted, AwaitingValidation, StockConfirmed, Paid, Shipped, Cancelled }; }
public static OrderStatus FromName(string name) public static OrderStatus From(int id)
{
var state = List().SingleOrDefault(s => s.Id == id);
if (state == null)
{ {
var state = List() throw new OrderingDomainException($"Possible values for OrderStatus: {String.Join(",", List().Select(s => s.Name))}");
.SingleOrDefault(s => String.Equals(s.Name, name, StringComparison.CurrentCultureIgnoreCase));
if (state == null)
{
throw new OrderingDomainException($"Possible values for OrderStatus: {String.Join(",", List().Select(s => s.Name))}");
}
return state;
} }
public static OrderStatus From(int id) return state;
{
var state = List().SingleOrDefault(s => s.Id == id);
if (state == null)
{
throw new OrderingDomainException($"Possible values for OrderStatus: {String.Join(",", List().Select(s => s.Name))}");
}
return state;
}
} }
} }

View File

@ -1,20 +1,16 @@
using MediatR; namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.Events;
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.BuyerAggregate;
namespace Ordering.Domain.Events public class BuyerAndPaymentMethodVerifiedDomainEvent
: INotification
{ {
public class BuyerAndPaymentMethodVerifiedDomainEvent public Buyer Buyer { get; private set; }
: INotification public PaymentMethod Payment { get; private set; }
{ public int OrderId { get; private set; }
public Buyer Buyer { get; private set; }
public PaymentMethod Payment { get; private set; }
public int OrderId { get; private set; }
public BuyerAndPaymentMethodVerifiedDomainEvent(Buyer buyer, PaymentMethod payment, int orderId) public BuyerAndPaymentMethodVerifiedDomainEvent(Buyer buyer, PaymentMethod payment, int orderId)
{ {
Buyer = buyer; Buyer = buyer;
Payment = payment; Payment = payment;
OrderId = orderId; OrderId = orderId;
}
} }
} }

View File

@ -1,15 +1,12 @@
using MediatR; namespace Microsoft.eShopOnContainers.Services.Ordering.Ordering.Domain.Events;
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
namespace Ordering.Domain.Events public class OrderCancelledDomainEvent : INotification
{ {
public class OrderCancelledDomainEvent : INotification public Order Order { get; }
{
public Order Order { get; }
public OrderCancelledDomainEvent(Order order) public OrderCancelledDomainEvent(Order order)
{ {
Order = order; Order = order;
}
} }
} }

View File

@ -1,15 +1,11 @@
using MediatR; namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.Events;
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate;
namespace Ordering.Domain.Events public class OrderShippedDomainEvent : INotification
{ {
public class OrderShippedDomainEvent : INotification public Order Order { get; }
{
public Order Order { get; }
public OrderShippedDomainEvent(Order order) public OrderShippedDomainEvent(Order order)
{ {
Order = order; Order = order;
}
} }
} }

View File

@ -1,8 +1,5 @@
using MediatR; 
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.Events
using System;
namespace Ordering.Domain.Events
{ {
/// <summary> /// <summary>
/// Event used when an order is created /// Event used when an order is created

View File

@ -1,23 +1,18 @@
namespace Ordering.Domain.Events namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.Events;
/// <summary>
/// Event used when the grace period order is confirmed
/// </summary>
public class OrderStatusChangedToAwaitingValidationDomainEvent
: INotification
{ {
using MediatR; public int OrderId { get; }
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; public IEnumerable<OrderItem> OrderItems { get; }
using System.Collections.Generic;
/// <summary> public OrderStatusChangedToAwaitingValidationDomainEvent(int orderId,
/// Event used when the grace period order is confirmed IEnumerable<OrderItem> orderItems)
/// </summary>
public class OrderStatusChangedToAwaitingValidationDomainEvent
: INotification
{ {
public int OrderId { get; } OrderId = orderId;
public IEnumerable<OrderItem> OrderItems { get; } OrderItems = orderItems;
public OrderStatusChangedToAwaitingValidationDomainEvent(int orderId,
IEnumerable<OrderItem> orderItems)
{
OrderId = orderId;
OrderItems = orderItems;
}
} }
} }

View File

@ -1,23 +1,17 @@
namespace Ordering.Domain.Events namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.Events;
/// <summary>
/// Event used when the order is paid
/// </summary>
public class OrderStatusChangedToPaidDomainEvent
: INotification
{ {
using MediatR; public int OrderId { get; }
using Microsoft.eShopOnContainers.Services.Ordering.Domain.AggregatesModel.OrderAggregate; public IEnumerable<OrderItem> OrderItems { get; }
using System.Collections.Generic;
/// <summary> public OrderStatusChangedToPaidDomainEvent(int orderId,
/// Event used when the order is paid IEnumerable<OrderItem> orderItems)
/// </summary>
public class OrderStatusChangedToPaidDomainEvent
: INotification
{ {
public int OrderId { get; } OrderId = orderId;
public IEnumerable<OrderItem> OrderItems { get; } OrderItems = orderItems;
public OrderStatusChangedToPaidDomainEvent(int orderId,
IEnumerable<OrderItem> orderItems)
{
OrderId = orderId;
OrderItems = orderItems;
}
} }
} }

View File

@ -1,16 +1,13 @@
namespace Ordering.Domain.Events namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.Events;
/// <summary>
/// Event used when the order stock items are confirmed
/// </summary>
public class OrderStatusChangedToStockConfirmedDomainEvent
: INotification
{ {
using MediatR; public int OrderId { get; }
/// <summary> public OrderStatusChangedToStockConfirmedDomainEvent(int orderId)
/// Event used when the order stock items are confirmed => OrderId = orderId;
/// </summary> }
public class OrderStatusChangedToStockConfirmedDomainEvent
: INotification
{
public int OrderId { get; }
public OrderStatusChangedToStockConfirmedDomainEvent(int orderId)
=> OrderId = orderId;
}
}

View File

@ -1,21 +1,18 @@
using System; namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.Exceptions;
namespace Ordering.Domain.Exceptions /// <summary>
/// Exception type for domain exceptions
/// </summary>
public class OrderingDomainException : Exception
{ {
/// <summary> public OrderingDomainException()
/// Exception type for domain exceptions { }
/// </summary>
public class OrderingDomainException : Exception
{
public OrderingDomainException()
{ }
public OrderingDomainException(string message) public OrderingDomainException(string message)
: base(message) : base(message)
{ } { }
public OrderingDomainException(string message, Exception innerException) public OrderingDomainException(string message, Exception innerException)
: base(message, innerException) : base(message, innerException)
{ } { }
}
} }

View File

@ -1,92 +1,87 @@
namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork;
public abstract class Entity
{ {
using MediatR; int? _requestedHashCode;
using System; int _Id;
using System.Collections.Generic; public virtual int Id
public abstract class Entity
{ {
int? _requestedHashCode; get
int _Id;
public virtual int Id
{ {
get return _Id;
{
return _Id;
}
protected set
{
_Id = value;
}
} }
protected set
private List<INotification> _domainEvents;
public IReadOnlyCollection<INotification> DomainEvents => _domainEvents?.AsReadOnly();
public void AddDomainEvent(INotification eventItem)
{ {
_domainEvents = _domainEvents ?? new List<INotification>(); _Id = value;
_domainEvents.Add(eventItem);
}
public void RemoveDomainEvent(INotification eventItem)
{
_domainEvents?.Remove(eventItem);
}
public void ClearDomainEvents()
{
_domainEvents?.Clear();
}
public bool IsTransient()
{
return this.Id == default(Int32);
}
public override bool Equals(object obj)
{
if (obj == null || !(obj is Entity))
return false;
if (Object.ReferenceEquals(this, obj))
return true;
if (this.GetType() != obj.GetType())
return false;
Entity item = (Entity)obj;
if (item.IsTransient() || this.IsTransient())
return false;
else
return item.Id == this.Id;
}
public override int GetHashCode()
{
if (!IsTransient())
{
if (!_requestedHashCode.HasValue)
_requestedHashCode = this.Id.GetHashCode() ^ 31; // XOR for random distribution (http://blogs.msdn.com/b/ericlippert/archive/2011/02/28/guidelines-and-rules-for-gethashcode.aspx)
return _requestedHashCode.Value;
}
else
return base.GetHashCode();
}
public static bool operator ==(Entity left, Entity right)
{
if (Object.Equals(left, null))
return (Object.Equals(right, null)) ? true : false;
else
return left.Equals(right);
}
public static bool operator !=(Entity left, Entity right)
{
return !(left == right);
} }
} }
private List<INotification> _domainEvents;
public IReadOnlyCollection<INotification> DomainEvents => _domainEvents?.AsReadOnly();
public void AddDomainEvent(INotification eventItem)
{
_domainEvents = _domainEvents ?? new List<INotification>();
_domainEvents.Add(eventItem);
}
public void RemoveDomainEvent(INotification eventItem)
{
_domainEvents?.Remove(eventItem);
}
public void ClearDomainEvents()
{
_domainEvents?.Clear();
}
public bool IsTransient()
{
return this.Id == default(Int32);
}
public override bool Equals(object obj)
{
if (obj == null || !(obj is Entity))
return false;
if (Object.ReferenceEquals(this, obj))
return true;
if (this.GetType() != obj.GetType())
return false;
Entity item = (Entity)obj;
if (item.IsTransient() || this.IsTransient())
return false;
else
return item.Id == this.Id;
}
public override int GetHashCode()
{
if (!IsTransient())
{
if (!_requestedHashCode.HasValue)
_requestedHashCode = this.Id.GetHashCode() ^ 31; // XOR for random distribution (http://blogs.msdn.com/b/ericlippert/archive/2011/02/28/guidelines-and-rules-for-gethashcode.aspx)
return _requestedHashCode.Value;
}
else
return base.GetHashCode();
}
public static bool operator ==(Entity left, Entity right)
{
if (Object.Equals(left, null))
return (Object.Equals(right, null)) ? true : false;
else
return left.Equals(right);
}
public static bool operator !=(Entity left, Entity right)
{
return !(left == right);
}
} }

View File

@ -1,70 +1,64 @@
using System; namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.SeedWork;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.SeedWork public abstract class Enumeration : IComparable
{ {
public abstract class Enumeration : IComparable public string Name { get; private set; }
public int Id { get; private set; }
protected Enumeration(int id, string name) => (Id, Name) = (id, name);
public override string ToString() => Name;
public static IEnumerable<T> GetAll<T>() where T : Enumeration =>
typeof(T).GetFields(BindingFlags.Public |
BindingFlags.Static |
BindingFlags.DeclaredOnly)
.Select(f => f.GetValue(null))
.Cast<T>();
public override bool Equals(object obj)
{ {
public string Name { get; private set; } if (obj is not Enumeration otherValue)
public int Id { get; private set; }
protected Enumeration(int id, string name) => (Id, Name) = (id, name);
public override string ToString() => Name;
public static IEnumerable<T> GetAll<T>() where T : Enumeration =>
typeof(T).GetFields(BindingFlags.Public |
BindingFlags.Static |
BindingFlags.DeclaredOnly)
.Select(f => f.GetValue(null))
.Cast<T>();
public override bool Equals(object obj)
{ {
if (obj is not Enumeration otherValue) return false;
{
return false;
}
var typeMatches = GetType().Equals(obj.GetType());
var valueMatches = Id.Equals(otherValue.Id);
return typeMatches && valueMatches;
} }
public override int GetHashCode() => Id.GetHashCode(); var typeMatches = GetType().Equals(obj.GetType());
var valueMatches = Id.Equals(otherValue.Id);
public static int AbsoluteDifference(Enumeration firstValue, Enumeration secondValue) return typeMatches && valueMatches;
{
var absoluteDifference = Math.Abs(firstValue.Id - secondValue.Id);
return absoluteDifference;
}
public static T FromValue<T>(int value) where T : Enumeration
{
var matchingItem = Parse<T, int>(value, "value", item => item.Id == value);
return matchingItem;
}
public static T FromDisplayName<T>(string displayName) where T : Enumeration
{
var matchingItem = Parse<T, string>(displayName, "display name", item => item.Name == displayName);
return matchingItem;
}
private static T Parse<T, K>(K value, string description, Func<T, bool> predicate) where T : Enumeration
{
var matchingItem = GetAll<T>().FirstOrDefault(predicate);
if (matchingItem == null)
throw new InvalidOperationException($"'{value}' is not a valid {description} in {typeof(T)}");
return matchingItem;
}
public int CompareTo(object other) => Id.CompareTo(((Enumeration)other).Id);
} }
public override int GetHashCode() => Id.GetHashCode();
public static int AbsoluteDifference(Enumeration firstValue, Enumeration secondValue)
{
var absoluteDifference = Math.Abs(firstValue.Id - secondValue.Id);
return absoluteDifference;
}
public static T FromValue<T>(int value) where T : Enumeration
{
var matchingItem = Parse<T, int>(value, "value", item => item.Id == value);
return matchingItem;
}
public static T FromDisplayName<T>(string displayName) where T : Enumeration
{
var matchingItem = Parse<T, string>(displayName, "display name", item => item.Name == displayName);
return matchingItem;
}
private static T Parse<T, K>(K value, string description, Func<T, bool> predicate) where T : Enumeration
{
var matchingItem = GetAll<T>().FirstOrDefault(predicate);
if (matchingItem == null)
throw new InvalidOperationException($"'{value}' is not a valid {description} in {typeof(T)}");
return matchingItem;
}
public int CompareTo(object other) => Id.CompareTo(((Enumeration)other).Id);
} }

View File

@ -1,6 +1,5 @@
namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork;
{
public interface IAggregateRoot { }
public interface IAggregateRoot { }
}

View File

@ -1,7 +1,6 @@
namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork;
public interface IRepository<T> where T : IAggregateRoot
{ {
public interface IRepository<T> where T : IAggregateRoot IUnitOfWork UnitOfWork { get; }
{
IUnitOfWork UnitOfWork { get; }
}
} }

View File

@ -1,12 +1,7 @@
using System; namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.Seedwork public interface IUnitOfWork : IDisposable
{ {
public interface IUnitOfWork : IDisposable Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken));
{ Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default(CancellationToken));
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken));
Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default(CancellationToken));
}
} }

View File

@ -1,48 +1,44 @@
using System.Collections.Generic; namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.SeedWork;
using System.Linq;
namespace Microsoft.eShopOnContainers.Services.Ordering.Domain.SeedWork public abstract class ValueObject
{ {
public abstract class ValueObject protected static bool EqualOperator(ValueObject left, ValueObject right)
{ {
protected static bool EqualOperator(ValueObject left, ValueObject right) if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))
{ {
if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null)) return false;
{ }
return false; return ReferenceEquals(left, null) || left.Equals(right);
} }
return ReferenceEquals(left, null) || left.Equals(right);
protected static bool NotEqualOperator(ValueObject left, ValueObject right)
{
return !(EqualOperator(left, right));
}
protected abstract IEnumerable<object> GetEqualityComponents();
public override bool Equals(object obj)
{
if (obj == null || obj.GetType() != GetType())
{
return false;
} }
protected static bool NotEqualOperator(ValueObject left, ValueObject right) var other = (ValueObject)obj;
{
return !(EqualOperator(left, right));
}
protected abstract IEnumerable<object> GetEqualityComponents(); return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
public override bool Equals(object obj) public override int GetHashCode()
{ {
if (obj == null || obj.GetType() != GetType()) return GetEqualityComponents()
{ .Select(x => x != null ? x.GetHashCode() : 0)
return false; .Aggregate((x, y) => x ^ y);
} }
var other = (ValueObject)obj; public ValueObject GetCopy()
{
return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents()); return this.MemberwiseClone() as ValueObject;
}
public override int GetHashCode()
{
return GetEqualityComponents()
.Select(x => x != null ? x.GetHashCode() : 0)
.Aggregate((x, y) => x ^ y);
}
public ValueObject GetCopy()
{
return this.MemberwiseClone() as ValueObject;
}
} }
} }