Ehsan Ghanbari

Open Source, Architecture, Patterns

Seldino

It was around 2015 that I embarked on a startup with the idea of helping people on choosing the bestselling products of real shops in an area. The idea was more extended and it supported by some investors at first. But after the first launch, the main investor broke his promises and the startup failed! I'm just talking about the project source code here as the architect of our team. Seldino was the name of the project.

 

 

Let me talk about the infrastructure layer of the project. This project has been built via the Domain Driven Design approach. The fundamental and vital classes and interfaces of a DDD project live in the infrastructure layer. For example in this project, I had put Domain, Email, Events, Helpers, Logging and Specification pattern of the project in that layer. For example, the domain business rule class to handle the business rules of the domain is like this:

 

 public class BusinessRule
    {
        private string _property;
        private string _rule;

        public BusinessRule(string property, string rule)
        {
            _property = property;
            _rule = rule;
        }
        public string Property
        {
            get { return _property; }
            set { _property = value; }
        }
        public string Rule
        {
            get { return _rule; }
            set { _rule = value; }
        }
    }

 

And the usage of the BusinessRule in EntityBase:

 

public abstract class EntityBase
    {
        private readonly List<BusinessRule> _brokenRules = new List<BusinessRule>();

        public Guid Id { get; set; }

        public DateTime CreationDate { get; set; }

        public DateTime LastUpdateDate { get; set; }

        public bool IsDeleted { get; set; }

        protected abstract void Validate();

        public virtual IEnumerable<BusinessRule> GetBrokenRules()
        {
            _brokenRules.Clear();
            Validate();
            return _brokenRules;
        }

        protected void AddBrokenRule(BusinessRule businessRule)
        {
            _brokenRules.Add(businessRule);
        }
    }
 

The aggregate interface is just a flag distinguish the aggregate root from other entities:

 

public interface IAggregateRoot
    {
    }

 

The base repository interface which will implement the layer supertype pattern in Repository Layer has the following definition:

 

 public interface IRepositoryBase<TEntity> where TEntity : IAggregateRoot
    {
        void Add(TEntity entity);

        void AddRange(IEnumerable<TEntity> entities);

        void AddBulk(IEnumerable<TEntity> entities);

        void AddBulkAsync(IEnumerable<TEntity> entities);

        void Edit(TEntity entity);

        void EditBulk(IEnumerable<TEntity> entities);

        void EditBulkAsync(IEnumerable<TEntity> entities);

        void Remove(Guid id);

        void Remove(Func<TEntity, Guid> predicate);

        void RemoveRange(IEnumerable<TEntity> entities);

        void RemoveBulk(IEnumerable<TEntity> entities);

        void RemoveBulkAsync(IEnumerable<TEntity> entities);

        void MergeBulk(IEnumerable<TEntity> entities);

        void MergeBulkAsync(IEnumerable<TEntity> entities);

        int Count(TEntity entity);

        bool Exist(Expression<Func<TEntity, bool>> expression);

        TEntity GetById(Guid id);

        TEntity Get(Expression<Func<TEntity, bool>> predicate);

        IList<TEntity> ListBy(Expression<Func<TEntity, bool>> query);

        IList<TEntity> GetAll();

        PagingQueryResponse<TEntity> GetAll(int pageIndex, int pageSize);

        PagingQueryResponse<TEntity> GetAll(int pageIndex, int pageSize, int count);

        PagingQueryResponse<TEntity> GetAll(Func<TEntity, bool> expressionQuery, int pageIndex, int pageSize);

        PagingQueryResponse<TEntity> GetAll(Expression<Func<TEntity, bool>> expressionQuery, int pageIndex, int pageSize);

        IList<TEntity> GetAll(ISpecification<TEntity> specification);

        PagingQueryResponse<TEntity> GetAll(ISpecification<TEntity> specification, int pageIndex, int pageSize);
    }

 

And about the Value Objects of the project we have a base class with the same name:

 

 public abstract class ValueObjectBase 
    {
        private readonly List<BusinessRule> _brokenRules = new List<BusinessRule>();

        public DateTime CreationDate { get; set; }
        public string Creator { get; set; }
        public bool IsDeleted { get; set; }

        protected static bool EqualOperator(ValueObjectBase left, ValueObjectBase right)
        {
            if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))
            {
                return false;
            }
            return ReferenceEquals(left, null) || left.Equals(right);
        }

        protected abstract void Validate();

        public void ThrowExceptionIfInvalid()
        {
            _brokenRules.Clear();
            Validate();

            if (_brokenRules.Count <= 0) return;
            var issues = new StringBuilder();
            foreach(var businessRule in _brokenRules)
            {
                issues.AppendLine(businessRule.Rule);
            }
            throw new ValueObjectIsInvalidException(issues.ToString());
        }
        protected void AddBrokenRule(BusinessRule businessRule)
        {
            _brokenRules.Add(businessRule);
        }
    }



One of the most important things in DDD is to handle Events of the Domain, the infrastructure of that  would like this:

 

 public interface IDomainEventHandlerFactory
    {
        IEnumerable<IDomainEventHandler<T>> GetDomainEventHandlersFor<T>(T domainEvent)
                                                                where T : IDomainEvent;
    }


   public static class IEnumerableExtensions
    {
        public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
        {
            foreach (T item in source)
                action(item);
        }
    }


 public interface IDomainEventHandler<T> where T : IDomainEvent
    {
        void Handle(T domainEvent);
    }


  public interface IDomainEvent
    {
    }


 public static class DomainEvents
    {
        public static IDomainEventHandlerFactory DomainEventHandlerFactory { get; set; }

        public static void Raise<T>(T domainEvent) where T : IDomainEvent
        {
            DomainEventHandlerFactory.GetDomainEventHandlersFor(domainEvent)
                                                    .ForEach(h => h.Handle(domainEvent));
        }
    }


And there are other configurations and infrastructural classes that you can refer to the source code in my git hub account. Let's continue our discussion with of the important aggregation:

 

namespace Seldino.Domain.OrderAggregation
{
    public class Order : EntityBase, IAggregateRoot
    {
        private readonly IList<OrderItem> _items;
        private Payment _payment;
        private IOrderState _state;

        public Order()
        {
            _items = new List<OrderItem>();
            _state = OrderStates.Open;
        }

        public User User { get; set; }

        public decimal ShippingCharge { get; set; }

        public Guid ShippingServiceId { get; set; }

        public virtual ShippingService ShippingService { get; set; }

        public Guid LocationId { get; set; }

        public virtual Location Location { get; set; }

        public Guid PaymentId { get; set; }

        public virtual Payment Payment
        {
            get { return _payment; }
            set { _payment = value; } 
        }

        public decimal ItemTotal()
        {
            return Items.Sum(i => i.LineTotal());
        }

        public decimal Total()
        {
            return Items.Sum(i => i.LineTotal()) + ShippingCharge;
        }

        public void SetPayment(Payment payment)
        {
            if (OrderHasBeenPaidFor())
                throw new OrderAlreadyPaidForException(GetDetailsOnExisitingPayment());

            if (OrderTotalMatches(payment))
                _payment = payment;
            else
                throw new PaymentAmountDoesNotEqualOrderTotalException(GetDetailsOnIssueWith(payment));

            _state.Submit(this);
        }

        private string GetDetailsOnExisitingPayment()
        {
            return String.Format(OrderBusinessRulesMessages.PaymentHasBeenPaid,
                                 Payment.Amount, Payment.PaymentDate, Payment.TransactionId);
        }

        private string GetDetailsOnIssueWith(Payment payment)
        {
            return String.Format(OrderBusinessRulesMessages.PaymentIsNotValid,
                Total(), payment.Amount, payment.TransactionId);
        }

        public bool OrderHasBeenPaidFor()
        {
            return Payment != null && OrderTotalMatches(Payment);
        }

        private bool OrderTotalMatches(Payment payment)
        {
            return Total() == payment.Amount;
        }

        public ICollection<OrderItem> Items
        {
            get { return _items; }
        }

        public OrderStatus Status
        {
            get { return _state.Status; }
        }

        public void AddItem(Product product, int qty)
        {
            if (_state.CanAddProduct())
            {
                if (!OrderContains(product))
                    _items.Add(OrderItemFactory.CreateItemFor(product, this, qty));
            }
            else
            {
                throw new CannotAmendOrderException(String.Format(OrderBusinessRulesMessages.CanNotAddItemToOrder, Status));
            }
        }

        private bool OrderContains(Product product)
        {
            return _items.Any(i => i.Contains(product));
        }

        internal void SetStateTo(IOrderState state)
        {
            _state = state;
        }

        public override string ToString()
        {
            var orderInfo = new StringBuilder();

            foreach (var item in _items)
            {
                orderInfo.AppendLine(String.Format("{0} از {1} ", item.Quantity, item.Product.Name));
            }

            orderInfo.AppendLine(String.Format("Sent : {0}", ShippingCharge));
            orderInfo.AppendLine(String.Format("Sum : {0}", Total()));

            return orderInfo.ToString();
        }

        protected override void Validate()
        {
            if (IsDeleted)
                AddBrokenRule(OrderBusinessRules.ProductHasBeenDeleted);

            if (CreationDate == DateTime.MinValue)
                AddBrokenRule(OrderBusinessRules.CreatedDateRequired);

            if (Location == null)
                base.AddBrokenRule(OrderBusinessRules.DeliveryAddressRequired);

            if (Items == null || !Items.Any())
                AddBrokenRule(OrderBusinessRules.ItemsRequired);
            else
            {
                if (Items.Any(i => i.GetBrokenRules().Any()))
                {
                    foreach (var item in Items.Where(i => i.GetBrokenRules().Any()))
                    {
                        foreach (var businessRule in item.GetBrokenRules())
                        {
                            AddBrokenRule(businessRule);
                        }
                    }
                }
            }
        }
    }
}

 

Take a deeper look at the usage of Business rules and the methods containing required business of the Order in an E-commerce. The Domain is not a class of properties at all! The Repository should introduce the Domain layer because its domain that should tell the repository layer what I want!

 

  public interface IOrderRepository : IRepositoryBase<Order>
    {
        Order GetOrder(Guid orderId);

        PagingQueryResponse<Order> GetOrders(PagingQueryRequest query);

        PagingQueryResponse<Order> GetInProcessOrders(PagingQueryRequest query);

        PagingQueryResponse<Order> GetPendingOrders(PagingQueryRequest query);

        PagingQueryResponse<Order> GetCompletedOrder(PagingQueryRequest query);

        PagingQueryResponse<Order> GetCancelledOrders(PagingQueryRequest query);

        IList<OrdersCountQueryModel> GetOrdersCount(Guid storeId);
    }


In an aggregate, lots of business exception could occur:

 

internal class PaymentAmountDoesNotEqualOrderTotalException : DomainExceptions
    {
        public PaymentAmountDoesNotEqualOrderTotalException(string message)
            : base(message)
        {
        }
    }

    internal class CannotAmendOrderException : DomainExceptions
    {
        public CannotAmendOrderException(string message)
            : base(message)
        {
        }
    }

    internal class OrderAlreadyPaidForException : DomainExceptions
    {
        public OrderAlreadyPaidForException(string message)
            : base(message)
        {
        }
    }

    internal class InvalidDeliveryAddressException : DomainExceptions
    {
        public InvalidDeliveryAddressException(string message)
            : base(message)
        {
        }
    }

 

 and about the defining business fields:

 

  public class OrderBusinessRules
    {
        public static readonly BusinessRule ProductHasBeenDeleted = new BusinessRule("IsDeleted", OrderBusinessRulesMessages.ProductHasBeenDeleted);
        public static readonly BusinessRule OrderRequired = new BusinessRule("OrderRequired", "An order item must be associated with an order.");
        public static readonly BusinessRule PriceNonNegative = new BusinessRule("Price", OrderBusinessRulesMessages.OrderMustHaveNonNegativePrice);
        public static readonly BusinessRule QtyNonNegative = new BusinessRule("Quantity", "An order item must have a positive qty value.");
        public static readonly BusinessRule ProductRequired = new BusinessRule("Product", OrderBusinessRulesMessages.OrderMustAssociatedWithProduct);
        public static readonly BusinessRule CreatedDateRequired = new BusinessRule("CreatedDate", OrderBusinessRulesMessages.OrderMustHaveACreationDate);
        public static readonly BusinessRule PaymentTransactionIdRequired = new BusinessRule("PaymentTransactionId", "If an order is set as paid it must have a corresponding payment transaction id.");
        public static readonly BusinessRule CustomerRequired = new BusinessRule("Customer", OrderBusinessRulesMessages.OrderMustAssociatedWithCustomer);
        public static readonly BusinessRule DeliveryAddressRequired = new BusinessRule("DeliveryAddress", OrderBusinessRulesMessages.OrderMustHaveDeliveryAddress);
        public static readonly BusinessRule ItemsRequired = new BusinessRule("Items", OrderBusinessRulesMessages.OrderMustHaveAnItem);
        public static readonly BusinessRule ShippingServiceRequired = new BusinessRule("ShippingService", OrderBusinessRulesMessages.ShippingServiceIsRequiredForOrder);
    }

 

As you saw the usage of them in Order AggregateRoot. We talked about the event of DDD in the infrastructure layer, there is an example of the usage in Order aggregate:

 

 public class OrderSubmittedEvent : IDomainEvent
    {
        public Order Order { get; set; }
    }



I have created a new namespace in Order aggregate which represents the state of an order:

 

 public interface IOrderState
    {
        int Id { get; set; }
        OrderStatus Status { get; }
        bool CanAddProduct();
        void Submit(Order order);
    }

 

And the OrderState class:

 

public abstract class OrderState : IOrderState
    {
        public virtual int Id { get; set; }

        public abstract OrderStatus Status { get; }

        public abstract bool CanAddProduct();

        public abstract void Submit(Order order);
    }


the OpenOrderState and SubmittedOrderState whose handle the order status of course:

 

public class OpenOrderState : OrderState
    {
        public override OrderStatus Status
        {
            get { return OrderStatus.InProcess; }
        }

        public override bool CanAddProduct()
        {
            return true;
        }

        public override void Submit(Order order)
        {
            if (order.OrderHasBeenPaidFor())
                order.SetStateTo(OrderStates.Submitted);

            DomainEvents.Raise(new OrderSubmittedEvent() { Order = order });
        }
    }



 public class SubmittedOrderState : OrderState
    {
        public override OrderStatus Status
        {
            get { return OrderStatus.Completed; }
        }

        public override bool CanAddProduct()
        {
            return false;
        }

        public override void Submit(Order order)
        {
            throw new InvalidOperationException(OrderBusinessRulesMessages.OrderHasBeeenSubmited);
        }
    }
 

And the OrderStates is like below

 

public class OrderStates
    {
        public static readonly IOrderState Open = new OpenOrderState() { Id = 1 };
        public static readonly IOrderState Submitted = new SubmittedOrderState() { Id = 2 };
    }

 

I just wanted to show you the usage of Domain Event by mentioning the OrderStatus. I've used the specification pattern in my domain which a useful pattern for making an object-specific in order to reusable query template. Now, in the Repository layer, I have created the base Repository class via layer supertype pattern:

 

internal abstract class RepositoryBase<TEntity> : IRepositoryBase<TEntity> where TEntity : EntityBase, IAggregateRoot
    {
        private DataContext _dataContext;
        private ReadOnlyDataContext _readOnlyDataContext;

        private readonly IDbSet<TEntity> _dbset;

        protected RepositoryBase(IDatabaseFactory databaseFactory)
        {
            DatabaseFactory = databaseFactory;
            _dbset = DataContext.Set<TEntity>();
        }

        protected IDatabaseFactory DatabaseFactory
        {
            get;
            private set;
        }

        protected DataContext DataContext => _dataContext ?? (_dataContext = DatabaseFactory.GetDataContext());

        protected ReadOnlyDataContext ReadOnlyDataContext => _readOnlyDataContext ?? (_readOnlyDataContext = DatabaseFactory.GetReadOnlyDataContext());

        public void Add(TEntity entity)
        {
            var entityBase = SetEntityBaseValueForAdd(entity);
            _dbset.Add(entityBase);
        }

        public void AddRange(IEnumerable<TEntity> entities)
        {
            ((DbSet<TEntity>)_dbset).AddRange(entities);
        }

        public void AddBulk(IEnumerable<TEntity> entities)
        {
            var bulkOperation = new BulkOperations();

            using (var trans = new TransactionScope())
            {
                using (var connection = new SqlConnection(ConfigurationManager.ConnectionStrings["Seldino"].ConnectionString))
                {
                    bulkOperation.Setup<TEntity>()
                        .ForCollection(entities)
                        .WithTable(typeof(TEntity).Name)
                        .AddAllColumns()
                        .BulkInsert()
                        .SetIdentityColumn(x => x.Id, ColumnDirection.Output)
                        .Commit(connection);
                }

                trans.Complete();
            }
        }

        public async void AddBulkAsync(IEnumerable<TEntity> entities)
        {
            var bulkOperation = new BulkOperations();

            using (var trans = new TransactionScope())
            {
                using (var connection = new SqlConnection(ConfigurationManager.ConnectionStrings["Seldino"].ConnectionString))
                {
                    bulkOperation.Setup<TEntity>()
                        .ForCollection(entities)
                       .WithTable(typeof(TEntity).Name)
                        .AddAllColumns()
                        .BulkInsert()
                        .SetIdentityColumn(x => x.Id, ColumnDirection.Output)
                        .Commit(connection);
                }

                trans.Complete();
            }
        }

        public void Edit(TEntity entity)
        {
            var entityBase = SetEntityBaseValueForEdit(entity);
            _dbset.Attach(entityBase);
            _dataContext.Entry(entityBase).State = EntityState.Modified;
        }

        public void EditBulk(IEnumerable<TEntity> entities)
        {
            var bulkOperation = new BulkOperations();

            using (var trans = new TransactionScope())
            {
                using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["Seldino"].ConnectionString))
                {
                    bulkOperation.Setup<TEntity>()
                        .ForCollection(entities)
                        .WithTable(typeof(TEntity).Name)
                        .AddColumn(x => x.CreationDate) //ToDo
                        .BulkUpdate()
                        .MatchTargetOn(x => x.Id)
                        .Commit(conn);
                }

                trans.Complete();
            }
        }

        public async void EditBulkAsync(IEnumerable<TEntity> entities)
        {
            var bulkOperation = new BulkOperations();

            using (var trans = new TransactionScope())
            {
                using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["Seldino"].ConnectionString))
                {
                    bulkOperation.Setup<TEntity>()
                        .ForCollection(entities)
                        .WithTable(typeof(TEntity).Name)
                        .AddColumn(x => x.CreationDate) //ToDo
                        .BulkUpdate()
                        .MatchTargetOn(x => x.Id)
                        .Commit(conn);
                }

                trans.Complete();
            }
        }

        public void Remove(Guid id)
        {
            var entity = _dbset.Find(id);
            _dbset.Remove(entity);
        }

        public void Remove(Func<TEntity, Guid> predicate)
        {
            throw new NotImplementedException();
        }

        public void RemoveRange(IEnumerable<TEntity> entities)
        {
            ((DbSet<TEntity>)_dbset).RemoveRange(entities);
        }

        public void RemoveBulk(IEnumerable<TEntity> entities)
        {
            var bulkOperation = new BulkOperations();

            using (var trans = new TransactionScope())
            {
                using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["Seldino"].ConnectionString))
                {
                    bulkOperation.Setup<TEntity>()
                        .ForCollection(entities)
                        .WithTable(typeof(TEntity).Name)
                        .AddColumn(x => x.Id)
                        .BulkDelete()
                        .MatchTargetOn(x => x.Id)
                        .Commit(conn);
                }

                trans.Complete();
            }
        }

        public async void RemoveBulkAsync(IEnumerable<TEntity> entities)
        {
            var bulkOperation = new BulkOperations();

            using (var trans = new TransactionScope())
            {
                using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["Seldino"].ConnectionString))
                {
                    bulkOperation.Setup<TEntity>()
                        .ForCollection(entities)
                        .WithTable(typeof(TEntity).Name)
                        .AddColumn(x => x.Id)
                        .BulkDelete()
                        .MatchTargetOn(x => x.Id)
                        .Commit(conn);
                }

                trans.Complete();
            }
        }

        public void MergeBulk(IEnumerable<TEntity> entities)
        {
            var bulkOperations = new BulkOperations();

            using (var trans = new TransactionScope())
            {
                using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["Seldino"].ConnectionString))
                {
                    bulkOperations.Setup<TEntity>()
                        .ForCollection(entities)
                        .WithTable(typeof(TEntity).Name)
                        .AddColumn(x => x.Id) //ToDo should pass the parameter
                        .BulkInsertOrUpdate()
                        .MatchTargetOn(x => x.Id)
                        .Commit(conn);
                }

                trans.Complete();
            }
        }

        public async void MergeBulkAsync(IEnumerable<TEntity> entities)
        {
            var bulkOperations = new BulkOperations();

            using (var trans = new TransactionScope())
            {
                using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["Seldino"].ConnectionString))
                {
                    bulkOperations.Setup<TEntity>()
                        .ForCollection(entities)
                        .WithTable(typeof(TEntity).Name)
                        .AddColumn(x => x.Id) //ToDo should pass the parameter
                        .BulkInsertOrUpdate()
                        .MatchTargetOn(x => x.Id)
                        .Commit(conn);
                }

                trans.Complete();
            }
        }

        public int Count(TEntity entity)
        {
            return _dbset.Count();
        }

        public bool Exist(Expression<Func<TEntity, bool>> expression)
        {
            return _dbset.Where(expression).Any();
        }

        public TEntity GetById(Guid id)
        {
            return _dbset.Find(id);
        }

        public TEntity Get(Expression<Func<TEntity, bool>> where)
        {
            return _dbset.Where(where).FirstOrDefault();
        }

        public IList<TEntity> ListBy(Expression<Func<TEntity, bool>> query)
        {
            throw new NotImplementedException();
        }

        public IList<TEntity> GetAll()
        {
            return _dbset.ToList();
        }

        public PagingQueryResponse<TEntity> GetAll(int pageIndex, int pageSize)
        {
            var total = _dbset.AsNoTracking().Count();

            var result = new PagingQueryResponse<TEntity>
            {
                PageSize = pageSize,
                CurrentPage = pageIndex,
                TotalCount = total,
                Result = _dataContext.Set<TEntity>().OrderBy(c => c.CreationDate)
                .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList()
            };

            return result;
        }

        public PagingQueryResponse<TEntity> GetAll(int pageIndex, int pageSize, int count)
        {
            var total = _dbset.AsNoTracking().Count();

            var result = new PagingQueryResponse<TEntity>
            {
                PageSize = pageSize,
                CurrentPage = pageIndex,
                TotalCount = total,
                Result = _dataContext.Set<TEntity>().OrderBy(c => c.CreationDate)
                .Skip((pageIndex - 1) * pageSize).Take(pageSize).Take(count).ToList()
            };

            return result;
        }

        public PagingQueryResponse<TEntity> GetAll(Func<TEntity, bool> expressionQuery, int pageIndex, int pageSize)
        {
            var total = _dbset.AsNoTracking().Count();

            var result = new PagingQueryResponse<TEntity>
            {
                PageSize = pageSize,
                CurrentPage = pageIndex,
                TotalCount = total,
                Result = _dataContext.Set<TEntity>().OrderBy(c => c.CreationDate)
                .Where(expressionQuery).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList()
            };

            return result;
        }

        public PagingQueryResponse<TEntity> GetAll(Expression<Func<TEntity, bool>> expressionQuery, int pageIndex, int pageSize)
        {
            var total = _dbset.AsNoTracking().Count();

            var result = new PagingQueryResponse<TEntity>
            {
                PageSize = pageSize,
                CurrentPage = pageIndex,
                TotalCount = total,
                Result = _dataContext.Set<TEntity>()
                .Where(expressionQuery).OrderBy(c => c.CreationDate)
                .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList()
            };

            return result;
        }

        public IList<TEntity> GetAll(ISpecification<TEntity> specification)
        {
            return _dbset.Where(specification.IsSatisfied()).ToList();
        }

        public PagingQueryResponse<TEntity> GetAll(ISpecification<TEntity> specification, int pageIndex, int pageSize)
        {
            var total = _dbset.AsNoTracking().Count();

            var result = new PagingQueryResponse<TEntity>
            {
                PageSize = pageSize,
                CurrentPage = pageIndex,
                TotalCount = total,
                Result = _dataContext.Set<TEntity>()
                .OrderBy(c => c.CreationDate).Where(specification.IsSatisfied())
                .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList()
            };

            return result;
        }

        private static TEntity SetEntityBaseValueForAdd(TEntity entity)
        {
            entity.Id = Guid.NewGuid();
            entity.CreationDate = DateTime.Now;
            entity.LastUpdateDate = DateTime.Now;
            entity.IsDeleted = false;
            return entity;
        }

        private static TEntity SetEntityBaseValueForEdit(TEntity entity)
        {
            entity.LastUpdateDate = DateTime.Now;
            return entity;
        }
    }

 

In bulk operations of the above base class, I've used sqlBulktools. In the Seldino project we used Autofac as DI container, and about the Repository config for IOC, we have the following class:

 

 public class RepositoryModule : Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            builder.RegisterType<UnitOfWork>().As<IUnitOfWork>().InstancePerRequest();
            builder.RegisterType<Log4NetAdapter>().As<ILogger>().SingleInstance();
            builder.RegisterType<CacheManager>().As<ICacheManager>().SingleInstance();
            builder.RegisterType<DatabaseFactory>().As<IDatabaseFactory>().SingleInstance();
            builder.RegisterType<LuceneIndexProvider>().As<IIndexProvider>().SingleInstance();

            builder.RegisterAssemblyTypes(ThisAssembly).Where(t => t.Name.EndsWith("Repository") && t.IsClass)
            .AsImplementedInterfaces().InstancePerRequest();
        }
    }

 

In DDD, for each of the aggregate roots we should create the repository:

 

 internal class OrderRepository : RepositoryBase<Order>, IOrderRepository
    {
        public OrderRepository(IDatabaseFactory databaseFactory)
            : base(databaseFactory)
        {
        }

        public Order GetOrder(Guid orderId)
        {
            return DataContext.Orders.SingleOrDefault(c => c.Id == orderId);
        }

        public PagingQueryResponse<Order> GetOrders(PagingQueryRequest query)
        {
            var specification = new OrderMatchingInOwnerSpecification(query.UserId);
            var totalCount = ReadOnlyDataContext.Orders.Where(specification.IsSatisfied());

            var result = new PagingQueryResponse<Order>
            {
                PageSize = query.PageSize,
                CurrentPage = query.PageIndex,
                TotalCount = totalCount.Count(),
                Result = DataContext.Orders
                    .Where(specification.IsSatisfied()).OrderByDescending(c => c.CreationDate)
                    .Skip((query.PageIndex - 1) * query.PageSize).Take(query.PageSize).ToList()
            };

            return result;
        }

        public PagingQueryResponse<Order> GetInProcessOrders(PagingQueryRequest query)
        {
            var specification = new OrderMatchingInOwnerSpecification(query.UserId).And(new OrderMatchingInInProcessStatusSpecification());
            var totalCount = ReadOnlyDataContext.Orders.Where(specification.IsSatisfied());

            var result = new PagingQueryResponse<Order>
            {
                PageSize = query.PageSize,
                CurrentPage = query.PageIndex,
                TotalCount = totalCount.Count(),
                Result = DataContext.Orders
                    .Where(specification.IsSatisfied()).OrderByDescending(c => c.CreationDate)
                    .Skip((query.PageIndex - 1) * query.PageSize).Take(query.PageSize).ToList()
            };

            return result;
        }

        public PagingQueryResponse<Order> GetPendingOrders(PagingQueryRequest query)
        {
            var specification = new OrderMatchingInOwnerSpecification(query.UserId);
            var totalCount = ReadOnlyDataContext.Orders.Where(specification.IsSatisfied());

            var result = new PagingQueryResponse<Order>
            {
                PageSize = query.PageSize,
                CurrentPage = query.PageIndex,
                TotalCount = totalCount.Count(),
                Result = DataContext.Orders
                    .Where(specification.IsSatisfied()).OrderByDescending(c => c.CreationDate)
                    .Skip((query.PageIndex - 1) * query.PageSize).Take(query.PageSize).ToList()
            };

            return result;
        }

        public PagingQueryResponse<Order> GetCompletedOrder(PagingQueryRequest query)
        {
            var specification = new OrderMatchingInOwnerSpecification(query.UserId).And(new OrderMatchingInCompletedStatusSpecification());
            var totalCount = ReadOnlyDataContext.Orders.Where(specification.IsSatisfied());

            var result = new PagingQueryResponse<Order>
            {
                PageSize = query.PageSize,
                CurrentPage = query.PageIndex,
                TotalCount = totalCount.Count(),
                Result = DataContext.Orders
                    .Where(specification.IsSatisfied()).OrderByDescending(c => c.CreationDate)
                    .Skip((query.PageIndex - 1) * query.PageSize).Take(query.PageSize).ToList()
            };

            return result;
        }

        public PagingQueryResponse<Order> GetCancelledOrders(PagingQueryRequest query)
        {
            var specification = new OrderMatchingInOwnerSpecification(query.UserId).And(new OrderMatchingInCancelledStatusSpecification());
            var totalCount = ReadOnlyDataContext.Orders.Where(specification.IsSatisfied());

            var result = new PagingQueryResponse<Order>
            {
                PageSize = query.PageSize,
                CurrentPage = query.PageIndex,
                TotalCount = totalCount.Count(),
                Result = DataContext.Orders
                    .Where(specification.IsSatisfied()).OrderByDescending(c => c.CreationDate)
                    .Skip((query.PageIndex - 1) * query.PageSize).Take(query.PageSize).ToList()
            };

            return result;
        }

        public IList<OrdersCountQueryModel> GetOrdersCount(Guid storeId)
        {
            //ToDo this query should be modified and pushed into linq to entity 

            var specification = new OrderMatchingInStoreSpecification(storeId);
            var query = ReadOnlyDataContext.Orders.Where(specification.IsSatisfied()).ToList();

            var model = new List<OrdersCountQueryModel>();

            if (query.Any())
            {
                model = new List<OrdersCountQueryModel>()
                {
                    new OrdersCountQueryModel
                    {
                        InProcess = query.Count(c => c.Status == OrderStatus.InProcess),
                        Completed = query.Count(c => c.Status == OrderStatus.Completed),
                        Cancelled = query.Count(c => c.Status == OrderStatus.Cancelled),
                       // StoreId = //ToDo store shoule has a relation with orders
                    }
                };

            }

            return model;
        }
    }


And every Entity and value object should get through the Repository. I used Entity Framework as ORM and the configuration or so-called the mapping of Order:

 

  internal class OrderConfiguration : EntityBaseConfiguration<Order>
    {
        public OrderConfiguration()
        {
            ToTable("Order", SchemaConstant.Order);
            Property(p => p.ShippingCharge).HasColumnType(SqlDbType.Decimal.ToString()).IsRequired();

            Property(d => d.ShippingServiceId).HasColumnType(SqlDbType.UniqueIdentifier.ToString()).IsOptional();
            HasRequired(d => d.ShippingService).WithMany().HasForeignKey(p => p.ShippingServiceId).WillCascadeOnDelete(false);

            Property(d => d.LocationId).HasColumnType(SqlDbType.UniqueIdentifier.ToString()).IsOptional();
            HasRequired(d => d.Location).WithMany().HasForeignKey(p => p.LocationId).WillCascadeOnDelete(false);

            Property(d => d.PaymentId).HasColumnType(SqlDbType.UniqueIdentifier.ToString()).IsOptional();
            HasRequired(d => d.Payment).WithMany().HasForeignKey(p => p.PaymentId).WillCascadeOnDelete(false);

            HasMany(pt => pt.Items).WithMany(p => p.Orders).Map(m =>
            {
                m.MapLeftKey("OrderId");
                m.MapRightKey("OrderItemId");
                m.ToTable("Order_OrdetItem", SchemaConstant.Order);
            });
        }
    }

    internal class OrderItemConfiguration : EntityBaseConfiguration<OrderItem>
    {
        public OrderItemConfiguration()
        {
            ToTable("OrderItem", SchemaConstant.Order);
            Property(s => s.Quantity).HasColumnType(SqlDbType.Int.ToString()).IsRequired();
            Property(s => s.Price).HasColumnType(SqlDbType.Decimal.ToString()).IsRequired();
        }
    }

 

We have two layers those interact with the Repository layer directly. They are Query and Command layers. I have described the pattern in this post before. In Command layer, for Order we have created the following commands:

 

 public interface IOrderCommand : ICommand
    {
        ChargeAccountCommand CreatePayment { get; set; }

        OrderStatus OrderStatus { get; set; }

        Guid DeliveryId { get; set; }

        Guid BasketId { get; set; }

        Guid UserId { get; set; }
    }

    [Validator(typeof(OrderCommandValidation))]
    public class OrderCommand : IOrderCommand
    {
        public ChargeAccountCommand CreatePayment { get; set; }

        public OrderStatus OrderStatus { get; set; }

        public Guid DeliveryId { get; set; }

        public Guid BasketId { get; set; }

        public Guid UserId { get; set; }
    }

    public class CreateOrderCommand : OrderCommand
    {
        public Guid[] ProductId { get; set; }
    }

    public class UpdateOrderCommand : OrderCommand
    {
        public Guid OrderId { get; set; }

        public Guid[] ProductId { get; set; }
    }

    public class CancelOrderCommand : ICommand
    {
        public Guid[] OrderIds { get; set; }
    }

    public class DeleteOrderCommand : ICommand
    {
        public Guid[] OrderIds { get; set; }
    }

 

And the above commands have been handled like below:

 

 internal class OrderCommandHandler :
        ICommandHandler<CreateOrderCommand>,
        ICommandHandler<UpdateOrderCommand>,
        ICommandHandler<CancelOrderCommand>,
        ICommandHandler<DeleteOrderCommand>
    {
        private readonly IOrderRepository _orderRepository;
        private readonly IBasketRepository _basketRepository;
        private readonly IMembershipRepository _membershipRepository;
        private readonly IDeliveryOptionRepository _deliveryOptionRepository;
        private readonly IUnitOfWork _unitOfWork;
        private readonly ILogger _logger;

        public OrderCommandHandler(IOrderRepository orderRepository,
            IBasketRepository basketRepository,
            IMembershipRepository membershipRepository,
            IDeliveryOptionRepository deliveryOptionRepository,
            IUnitOfWork unitOfWork,
            ILogger logger)
        {
            _orderRepository = orderRepository;
            _basketRepository = basketRepository;
            _membershipRepository = membershipRepository;
            _deliveryOptionRepository = deliveryOptionRepository;
            _unitOfWork = unitOfWork;
            _logger = logger;
        }

        /// <summary>
        /// Create order by registed members
        /// </summary>
        /// <param name="command"></param>
        /// <returns></returns>
        public ICommandResult Execute(CreateOrderCommand command)
        {
            try
            {
                if (command == null)
                {
                    throw new ArgumentNullException();
                }

                var basket = _basketRepository.GetById(command.BasketId);
                var user = _membershipRepository.GetUserById(command.UserId);
                var deliveryAddress = user.Locations.FirstOrDefault(d => d.Id == command.DeliveryId);

                var order = ConvertToOrder(basket);
                order.User = user;
                order.Location = deliveryAddress;
                _orderRepository.Add(order);
                _basketRepository.Remove(basket.Id);

                _unitOfWork.Commit();
                return new SuccessResult(OrderCommandMessage.OrderCreatedSuccessfully);
            }
            catch (Exception exception)
            {
                _logger.Error(exception.Message);
                return new FailureResult(OrderCommandMessage.OrderCreationFailed);
            }
        }

        private static Order ConvertToOrder(Basket basket)
        {
            var order = new Order();
            //{
            //    ShippingCharge = basket.DeliveryCost(),
            //    ShippingService = basket.DeliveryOption.ShippingService
            //};

            foreach (var item in basket.Items())
            {
                order.AddItem(item.Product, item.Quantity.Value);
            }

            return order;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="command"></param>
        /// <returns></returns>
        public ICommandResult Execute(UpdateOrderCommand command)
        {
            try
            {
                if (command == null)
                {
                    throw new ArgumentNullException();
                }

                var order = new Order { Id = command.OrderId };//, Status = OrderStatus.Processing };
                _orderRepository.Edit(order);
                _unitOfWork.Commit();
                return new SuccessResult(OrderCommandMessage.OrderEditedSuccessfully);
            }
            catch (Exception exception)
            {
                _logger.Error(exception.Message);
                return new FailureResult(OrderCommandMessage.OrderEditionFailed);
            }
        }

        /// <summary>
        /// Canceling an order identity
        /// </summary>
        /// <param name="command"></param>
        /// <returns></returns>
        public ICommandResult Execute(CancelOrderCommand command)
        {
            if (command == null)
            {
                throw new ArgumentNullException();
            }

            var exceptions = new List<Exception>();
            foreach (var orderId in command.OrderIds)
            {
                try
                {
                    var order = CancelOrder(orderId);
                    _orderRepository.Edit(order);
                }
                catch (Exception exception)
                {
                    exceptions.Add(exception);
                    return new FailureResult(OrderCommandMessage.OrderCancellationFailed);
                }
            }

            _unitOfWork.Commit();
            return new SuccessResult(OrderCommandMessage.OrderCanceledSuccessfully);
        }

        /// <summary>
        /// Delete an order 
        /// </summary>
        /// <param name="command"></param>
        /// <returns></returns>
        public ICommandResult Execute(DeleteOrderCommand command)
        {
            if (command == null)
            {
                throw new ArgumentNullException();
            }

            var exceptions = new List<Exception>();
            foreach (var orderId in command.OrderIds)
            {
                try
                {
                    var order = DeleteOrder(orderId);
                    _orderRepository.Edit(order);
                }
                catch (Exception exception)
                {
                    exceptions.Add(exception);
                    return new FailureResult(OrderCommandMessage.OrderDeletionFailed);
                }
            }

            _unitOfWork.Commit();
            return new SuccessResult(OrderCommandMessage.OrderDeletedSuccessfully);
        }

        private Order CancelOrder(Guid orderId)
        {
            var order = CheckOrderStatus(orderId);
            // order.Status = OrderStatus.Cancelled;
            return order;
        }

        private Order DeleteOrder(Guid orderId)
        {
            var order = CheckOrderStatus(orderId);
            order.IsDeleted = true;
            return order;
        }

        private DeliveryOption GetCheapestDeliveryOption()
        {
            return _deliveryOptionRepository.GetAll().OrderBy(d => d.Cost).FirstOrDefault();
        }

        private Order CheckOrderStatus(Guid orderId)
        {
            var order = _orderRepository.GetById(orderId);

            if (order.Status == OrderStatus.Completed)
                throw new OrderHasBeenCompletedException(OrderExceptionMessage.OrderOperationHasBeenCompleted);

            return order;
        }
    }

 

As I told before, we used Autofac in this project and here is the configuration of that for Command layer:

 

 public class CommandModule : Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            builder.RegisterModule<RepositoryModule>();
            builder.RegisterType<CommandBus>().As<ICommandBus>().InstancePerRequest();
            builder.RegisterAssemblyTypes(ThisAssembly).AsClosedTypesOf(typeof(ICommandHandler<>)).InstancePerRequest();
        }
    }


And about the query layer, we only retrieve the data and no state would change via this layer. The request and response pattern has been used in this pattern:

 

 public class OrderQueryRequest : QueryRequest
    {
        public OrderQueryRequest(Guid orderId)
        {
            OrderId = orderId;
        }

        public Guid OrderId { get; set; }
    }

    public class OrdersQueryRequest : PagingQueryRequest
    {
        public OrdersQueryRequest(int pageIndex, int pageSize)
            : base(pageIndex, pageSize)
        {
            PageIndex = pageIndex;
            PageSize = pageSize;
        }

        public OrdersQueryRequest(int pageIndex, int pageSize, Guid userId)
            : base(pageIndex, pageSize)
        {
            PageIndex = pageIndex;
            PageSize = pageSize;
            UserId = userId;
        }
    }

public class OrderQueryResponse : QueryResponse
    {
        public OrderDto Order { get; set; }
    }

    public class OrdersQueryResponse: QueryResponse
    {
        public PagingQueryResponse<OrderDto> Orders { get; set; } 
    }

 

We used DTO objects in service layer:

 

 public class OrderDto
    {
        public Guid Id { get; set; }

        public string PaymentTransactionId { get; set; }

        public bool OrderHasBeenPaidFor { get; set; }

        public IEnumerable<OrderItemDto> Items { get; set; }

        public string ShippingCharge { get; set; }

        public string ShippingServiceCourierName { get; set; }

        public string ShippingServiceDescription { get; set; }

        public string Total { get; set; }

    }

    public class OrderItemDto
    {

        public int Quantity { get; set; }

        public Guid Id { get; set; }

        public string ProductName { get; set; }

        public string Price { get; set; }
    }

    public class OrderSummaryDto
    {
        public Guid Id { get; set; }

        public DateTime Created { get; set; }

        public bool IsSubmitted { get; set; }
    }

    public class OrdersCountDto
    {
        public Guid StoreId { get; set; }

        public string StoreName { get; set; }

        public int Completed { get; set; }

        public int InProcess { get; set; }

        public int Cancelled { get; set; }
    }

 

And the interface of the order service:

 

 public interface IOrderQueryService
    {
        OrderQueryResponse GetOrderById(OrderQueryRequest request);

        OrdersQueryResponse GetOrders(OrdersQueryRequest request);

        OrdersQueryResponse GetInProcessOrders(OrdersQueryRequest request);

        OrdersQueryResponse GetPendingOrders(OrdersQueryRequest request);

        OrdersQueryResponse GetCompletedOrders(OrdersQueryRequest request);

        OrdersQueryResponse GetCancelledOrders(OrdersQueryRequest request);

        OrderQueryResponse PursuitOrder(OrderQueryRequest request);
    }


 

And finally the implementation of the above interface as service class:

 

internal class OrderQueryService : IOrderQueryService
    {
        private readonly IOrderRepository _orderRepository;
        private readonly ILogger _logger;

        public OrderQueryService(IOrderRepository orderRepository, ILogger logger)
        {
            _orderRepository = orderRepository;
            _logger = logger;
        }

        public OrderQueryResponse GetOrderById(OrderQueryRequest request)
        {
            var response = new OrderQueryResponse();

            try
            {
                var order = _orderRepository.GetById(request.OrderId);
                response.Order = Mapper.Map<Order, OrderDto>(order);
            }
            catch (Exception exception)
            {
                _logger.Log(exception);
            }

            return response;
        }

        public OrdersQueryResponse GetOrders(OrdersQueryRequest request)
        {
            var response = new OrdersQueryResponse();

            try
            {
                var orders = _orderRepository.GetOrders(request);
                response.Orders = Mapper.Map<PagingQueryResponse<Order>, PagingQueryResponse<OrderDto>>(orders);
            }
            catch (Exception exception)
            {
                response.Failed = true;
                _logger.Log(exception);
            }

            return response;
        }

        public OrdersQueryResponse GetInProcessOrders(OrdersQueryRequest request)
        {
            var response = new OrdersQueryResponse();

            try
            {
                var orders = _orderRepository.GetInProcessOrders(request);
                response.Orders = Mapper.Map<PagingQueryResponse<Order>, PagingQueryResponse<OrderDto>>(orders);
            }
            catch (Exception exception)
            {
                response.Failed = true;
                _logger.Log(exception);
            }

            return response;
        }

        public OrdersQueryResponse GetPendingOrders(OrdersQueryRequest request)
        {
            var response = new OrdersQueryResponse();

            try
            {
                var orders = _orderRepository.GetPendingOrders(request);
                response.Orders = Mapper.Map<PagingQueryResponse<Order>, PagingQueryResponse<OrderDto>>(orders);
            }
            catch (Exception exception)
            {
                _logger.Log(exception);
                response.Failed = true;
            }

            return response;
        }

        public OrdersQueryResponse GetCompletedOrders(OrdersQueryRequest request)
        {
            var response = new OrdersQueryResponse();

            try
            {
                var orders = _orderRepository.GetCompletedOrder(request);
                response.Orders = Mapper.Map<PagingQueryResponse<Order>, PagingQueryResponse<OrderDto>>(orders);
            }
            catch (Exception exception)
            {
                _logger.Log(exception);
                response.Failed = true;
            }

            return response;
        }

        public OrdersQueryResponse GetCancelledOrders(OrdersQueryRequest request)
        {
            var response = new OrdersQueryResponse();

            try
            {
                var orders = _orderRepository.GetCompletedOrder(request);
                response.Orders = Mapper.Map<PagingQueryResponse<Order>, PagingQueryResponse<OrderDto>>(orders);
            }
            catch (Exception exception)
            {
                _logger.Log(exception);
                response.Failed = true;
            }

            return response;
        }

        public OrderQueryResponse PursuitOrder(OrderQueryRequest request)
        {
            var response = new OrderQueryResponse();

            try
            {
                var order = _orderRepository.GetById(request.OrderId);
                response.Order = Mapper.Map<Order, OrderDto>(order);
            }
            catch (Exception exception)
            {
                response.Failed = true;
                _logger.Log(exception);
            }

            return response;
        }
    }
 

 

the MVC controllers will use the command to change the state of an entity and the queries will be used only for getting the data:

 

public class OrderController : BaseController
    {
        private readonly ICommandBus _commandBus;
        private readonly IOrderQueryService _orderQueryService;

        public OrderController(ICommandBus commandBus, IOrderQueryService orderQueryService)
        {
            _commandBus = commandBus;
            _orderQueryService = orderQueryService;
        }

        [HttpPost]
        public ActionResult Create(CreateOrderCommand command)
        {
            if (!ModelState.IsValid) return JsonMessage("");
            var result = _commandBus.Send(command);
            return JsonMessage(result);
        }

        public ViewResult Invoice()
        {
            return View("Invoice");
        }

        [HttpPost]
        public JsonResult Delete(DeleteOrderCommand command)
        {
            var result = _commandBus.Send(command);
            return JsonMessage(result);
        }

        /// <summary>
        /// Canceling the order
        /// </summary>
        /// <param name="command"></param>
        /// <returns></returns>
        [HttpPost]
        public JsonResult Cancel(CancelOrderCommand command)
        {
            var result = _commandBus.Send(command);
            return JsonMessage(result);
        }

        /// <summary>
        /// Pursuiting order 
        /// </summary>
        /// <param name="orderId"></param>
        /// <returns></returns>
        [OutputCache(Duration = 900, Location = OutputCacheLocation.Client, VaryByParam = "*")]
        public ActionResult Pursuit(Guid orderId)
        {
            var order = _orderQueryService.PursuitOrder(new OrderQueryRequest(orderId));
            return View("Pursuit", order);
        }

      }
 

I didn't cover even the whole infrastructural concepts of the project. Lots of patterns and principals have been used in this project I just tried to make an overview of that. You can see the source code in my github account and ask me if you would have any sort of question. Cheers!

About Me

Ehsan Ghanbari

Hi! my name is Ehsan. I'm a developer, passionate technologist, and fan of clean code. I'm interested in enterprise and large-scale applications architecture and design patterns and I'm spending a lot of my time on architecture subject. Since 2008, I've been as a developer for companies and organizations and I've been focusing on Microsoft ecosystem all the time. During the&nb Read More

Post Tags
Pending Interest Posts
Game
Limoee
comments powered by Disqus