12
\$\begingroup\$

After doing a whole bunch of research on Onion Architecture, I have made an attempt at implementing this in a new system that we are developing.

We have the Layers as per below:

  • Domain
    • Domain.Entities
    • Domain.Interfaces
  • Infrastructure
    • Infrastructure.Data
    • Infrastructure.DependencyResolution
    • Infrastructure.Interfaces
    • Infrastructure.Logging
  • Services
    • Services.Interfaces
  • Tests
    • Tests.Core
  • Web
    • Web.UI

Domain.Entities - All the domain models are kept here.

Base Entity

 namespace Domain.Entities
 {
    public class BaseEntity
    {
        public virtual Guid ID { get; set; }

        public virtual DateTime DateCreated { get; set; }

        public virtual DateTime? DeletedDate { get; set; } 

        public override bool Equals (object obj)
        {
            if (obj == null)
                return false;  
            var t = obj as BaseEntity;  
            if (t == null)
                return false;  
            if (ID == t.ID)
                return true;  
            return false;  
        }

        public override int GetHashCode ()
        {
            int hash = GetType ().GetHashCode ();
            hash = (hash * 397) ^ ID.GetHashCode ();
            return hash;
        }
    }
}

Company Entity

namespace Domain.Entities
{
    public class Company : BaseEntity
    {
        public virtual string Name { get; set; }

        public virtual IEnumerable<User> Users { get; set; }

        public virtual IEnumerable<Branch> Branches { get; set; }

        public virtual IEnumerable<Department> Departments { get; set; }

        public class CompanyCollection : List<Company>
        {

        }
    }
}

Domain.Interfaces - I have a bunch of generic interfaces programmed here.

  • IReadRepository
  • IReadWriteRepsitory
  • IUnitOfWork
  • IWriteRepository

IReadRepository

namespace Domain.Interfaces
{
    public interface IReadRepository<TEntity> where TEntity : class
    {
        IQueryable<TEntity> All ();

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

        TEntity FindBy (object id);

        IQueryable<TEntity> FilterBy (Expression<Func<TEntity, bool>> expression);
    }
}

IWriteRepository

namespace Domain.Interfaces
{
    public interface IWriteRepository<TEntity> where TEntity : class
    {
        bool Add (TEntity entity);

        bool Add (IEnumerable<TEntity> entities);

        bool Update (TEntity entity);

        bool Update (IEnumerable<TEntity> entities);

        bool Delete (TEntity entity);

        bool Delete (IEnumerable<TEntity> entities);
    }
}

IReadWriteRepository

namespace Domain.Interfaces
{
    public interface IReadWriteRepository<TEntity> : 
                        IReadRepository<TEntity>, IWriteRepository<TEntity> where TEntity : class
    {

    }
}

IUnitOfWork

namespace Domain.Interfaces
{
    public interface IUnitOfWork : IDisposable
    {
        void Commit ();

        void Rollback ();
    }
}

Infrastructure.Interfaces - Store all infrastructure service interfaces here

namespace Infrastructure.Interfaces
{
    public interface IConfigService
    {
        string Connection{ get; }
    }
}

namespace Infrastructure.Interfaces
{
    public interface ILoggingService
    {
        bool IsDebugEnabled { get; }

        bool IsErrorEnabled { get; }

        bool IsFatalEnabled { get; }

        bool IsInfoEnabled { get; }

        bool IsTraceEnabled { get; }

        bool IsWarnEnabled { get; }

        void Debug (Exception exception);

        void Debug (string format, params object[] args);

        void Debug (Exception exception, string format, params object[] args);

        void Error (Exception exception);

        void Error (string format, params object[] args);

        void Error (Exception exception, string format, params object[] args);

        void Fatal (Exception exception);

        void Fatal (string format, params object[] args);

        void Fatal (Exception exception, string format, params object[] args);

        void Info (Exception exception);

        void Info (string format, params object[] args);

        void Info (Exception exception, string format, params object[] args);

        void Trace (Exception exception);

        void Trace (string format, params object[] args);

        void Trace (Exception exception, string format, params object[] args);

        void Warn (Exception exception);

        void Warn (string format, params object[] args);

        void Warn (Exception exception, string format, params object[] args);
    }
}

Infrastructure.Data - I'm using Fluent NHibernate to talk to a postgresql database.

In here I keep my Mapping Files, Unit of work implementation and the Generic repository implementation.

Hibernate Helper

namespace Infrastructure.Data.Helpers
{
    public class NHibernateHelper
    {
        private ISessionFactory _sessionFactory;
        private readonly string _connectionString;

        public NHibernateHelper (string connectionString)
        {
            if (string.IsNullOrEmpty (connectionString))
                throw new HibernateConfigException ("ConnectionString in Web.config is not set.");

            _connectionString = connectionString;
        }

        public ISessionFactory SessionFactory {
            get {
                return _sessionFactory ?? (_sessionFactory = InitializeSessionFactory ());
            }
        }

        private ISessionFactory InitializeSessionFactory ()
        {
            return Fluently.Configure () . . . . . . .;
        }
    }

}

Unit Of Work

namespace Infrastructure.Data.Helpers
{
    public class UnitOfWork : IUnitOfWork
    {
        private readonly ISessionFactory _sessionFactory;
        private readonly ITransaction _transaction;

        public ISession Session { get; private set; }

        public UnitOfWork (ISessionFactory sessionFactory)
        {
            _sessionFactory = sessionFactory;
            Session = _sessionFactory.OpenSession ();
            Session.FlushMode = FlushMode.Auto;
            _transaction = Session.BeginTransaction (IsolationLevel.ReadCommitted);
        }

        public void Commit ()
        {
            if (!_transaction.IsActive) {
                throw new InvalidOperationException ("Oops! We don't have an active transaction");
            }
            _transaction.Commit ();
        }

        public void Rollback ()
        {
            if (_transaction.IsActive) {
                _transaction.Rollback ();
            }
        }

        public void Dispose ()
        {
            if (Session.IsOpen) {
                Session.Close ();
                Session = null;
            }
        }
    }
}

Base Mapping

namespace Infrastructure.Data.Mapping
{
    public class BaseMapping : ClassMap<BaseEntity>
    {
        public BaseMapping ()
        {
            UseUnionSubclassForInheritanceMapping ();
            Id (x => x.ID);
            Map (x => x.DateCreated);
            Map (x => x.DeletedDate);
        }
    }
}

Company Mapping

namespace Infrastructure.Data.Mapping
{
    public class CompanyMapping : SubclassMap<Company>
    {
        public CompanyMapping ()
        {
            Abstract ();
            Map (x => x.Name);

            HasManyToMany<User> (x => x.Users).Table ("CompanyUser").Inverse ();

            HasMany<Branch> (x => x.Branches).Inverse ().Cascade.All ();
            HasMany<Department> (x => x.Departments).Inverse ().Cascade.All ();

            Table ("Company");
        }
    }
}

Repository Implementation

namespace Infrastructure.Data.Repositories
{
    public class Repository<TEntity> : IReadWriteRepository<TEntity>
        where TEntity : class
    {
        private readonly ISession _session;

        public Repository (ISession session)
        {
            _session = session;
        }

        #region IWriteRepository

        public bool Add (TEntity entity)
        {
            _session.Save (entity);
            return true;
        }

        public bool Add (System.Collections.Generic.IEnumerable<TEntity> entities)
        {
            foreach (TEntity entity in entities) {
                _session.Save (entity);
            }
            return true;
        }

        public bool Update (TEntity entity)
        {
            _session.Update (entity);
            return true;
        }

        public bool Update (System.Collections.Generic.IEnumerable<TEntity> entities)
        {
            foreach (TEntity entity in entities) {
                _session.Update (entity);
            }
            return true;
        }

        public bool Delete (TEntity entity)
        {
            _session.Delete (entity);
            return true;
        }

        public bool Delete (System.Collections.Generic.IEnumerable<TEntity> entities)
        {
            foreach (TEntity entity in entities) {
                _session.Delete (entity);
            }
            return true;
        }

        #endregion

        #region IReadRepository

        public System.Linq.IQueryable<TEntity> All ()
        {
            return _session.Query<TEntity> ();
        }

        public TEntity FindBy (System.Linq.Expressions.Expression<System.Func<TEntity, bool>> expression)
        {
            return FilterBy (expression).SingleOrDefault ();
        }

        public TEntity FindBy (object id)
        {
            return _session.Get<TEntity> (id);
        }

        public System.Linq.IQueryable<TEntity> FilterBy (System.Linq.Expressions.Expression<System.Func<TEntity, bool>> expression)
        {
            return All ().Where (expression).AsQueryable ();
        }

        #endregion
    }
}

Finally, the service for dependency injection:

namespace Infrastructure.Data.Services
{
    public class ConfigService : IConfigService
    {
        #region IConfigService implementation

        public string Connection {
            get {
                string strConnectionString = null;
                var connectionSettings = ConfigurationManager.ConnectionStrings ["Connection"];

                if (connectionSettings != null) {
                    strConnectionString = connectionSettings.ConnectionString;
                }

                return strConnectionString;
            }
        }

        #endregion
    }
}

Infrastructure.DependencyResolution - Using Simple Injector

In here I store all my registration packages / modules

ConfigPackage

namespace Infrastructure.DependecyResolution
{
    public class ConfigPackage : IPackage
    {
        #region IPackage implementation

        public void RegisterServices (SimpleInjector.Container container)
        {
            container.Register<IConfigService,ConfigService> ();
        }

        #endregion
    }
}

RepositoryPackage

namespace Infrastructure.DependecyResolution
{
    public class RepositoryPackage : IPackage
    {
        #region IPackage implementation

        public void RegisterServices (SimpleInjector.Container container)
        {

            container.RegisterPerWebRequest<ISessionFactory> (() => {

                var configPackage = container.GetInstance<IConfigService> ();
                NHibernateHelper objNHibernate = new NHibernateHelper (configPackage.Connection);
                return objNHibernate.SessionFactory;
            });

            container.RegisterPerWebRequest<IUnitOfWork, UnitOfWork> ();

            container.RegisterPerWebRequest<ISession> (() => {

                UnitOfWork unitOfWork = (UnitOfWork)container.GetInstance<IUnitOfWork> ();
                return unitOfWork.Session;

            });

            container.RegisterOpenGeneric (typeof(IReadWriteRepository<>), typeof(Repository<>));
        }

        #endregion
    }
}

LoggingPackage

namespace Infrastructure.DependecyResolution
{
    public class LoggingPackage : IPackage
    {
        #region IPackage implementation

        public void RegisterServices (SimpleInjector.Container container)
        {
            ILoggingService logger = GetLoggingService ();

            container.Register<ILoggingService> (() => logger);
        }

        #endregion

        private ILoggingService GetLoggingService ()
        {
            ConfigurationItemFactory.Default.LayoutRenderers.RegisterDefinition ("utc_date", typeof(UtcDateRenderer));
            ConfigurationItemFactory.Default.LayoutRenderers.RegisterDefinition ("web_variables", typeof(WebVariablesRenderer));

            ILoggingService logger = (ILoggingService)LogManager.GetLogger ("NLogLogger", typeof(LoggingService));

            return logger;
        }
    }
}

Infrastructure.Logging - using NLog

In here is just the implementation of the logging interface.

Services.Interfaces - Applicaiton Service interfaces gets stored here

namespace Services.Interfaces
{
    public interface ICompanyService
    {
        IQueryable<Company> GetCompanies ();

        Company GetCompany (Guid guidId);

        void CreateNewCompany (Company company);

        void UpdateExistingCompany (Company company);

        void DeleteCompany (Company company);
    }
}

Tests.Core

Repository and Service Tests are in here at this stage, sure there will be more tests going in here.

Web.UI - ASP.NET MVC 4 Project as user interface application

Implementation of the Company Service Interface.

namespace Web.UI.Services
{
    public class CompanyService : ICompanyService
    {
        private IUnitOfWork _unitOfWork;
        private IReadWriteRepository<Company> _companyRepository;

        public CompanyService (IUnitOfWork unitOfWork, IReadWriteRepository<Company> companyRepository)
        {
            _unitOfWork = unitOfWork;
            _companyRepository = companyRepository;
        }

        #region ICompanyService implementation

        public IQueryable<Company> GetCompanies ()
        {
            return _companyRepository.All ().Where (x => !x.DeletedDate.HasValue);
        }

        public Company GetCompany (Guid guidId)
        {
            return _companyRepository.FindBy (guidId);
        }

        public void CreateNewCompany (Company company)
        {
            if (_companyRepository.Add (company))
                _unitOfWork.Commit ();
            else
                _unitOfWork.Rollback ();
        }

        public void UpdateExistingCompany (Company company)
        {
            if (_companyRepository.Update (company))
                _unitOfWork.Commit ();
            else
                _unitOfWork.Rollback ();
        }

        public void DeleteCompany (Company company)
        {
            if (_companyRepository.Update (company))
                _unitOfWork.Commit ();
            else
                _unitOfWork.Rollback ();
        }

        #endregion
    }
}

The Inject the ICompanyService into the Controller. Create a view model for Company which the view can manipulate.

Simple Injector Service also gets initialized in this project. While it gets initialized I register the Services.Interfaces to Web.UI.Services implementation.

That is pretty much how I have implemented the Onion Architecture. Let me know if I went wrong somewhere. This is the first time I set up an application with a proper architecture.

\$\endgroup\$
10
\$\begingroup\$

Let me know if I went wrong somewhere. This is the first time I set up an application with a proper architecture.

I'm afraid there is no such thing as general "proper" architecture. Relevant architecture is the one that enables/assists developers in implementing new functionality or adjusting your solution to new requirements.

In your implementation I don't see the reason to define your own logging and repository/UoW patterns:

Note that the series of articles "Refactoring toward frictionless & odorless code" actually show how you can move the transaction/session management to infrastructure level, leaving your business logic clean an tidy.

And a couple of small comments:

  • I don't see any reason to have public class CompanyCollection : List<Company> {}
  • Repository - why do you return true in Update methods? If there is a failure - throw exception, not return false.
  • IConfigService/ConfigService breaks Open/Closed principle, as would need to edit this class (add properties) whenever a new configuration parameter is needed. Classes that require configuration should expect specific configuration values in constructor instead of consuming the general IConfigService. Inject these parameters at the DI registration time. If you need a dynamic configuration which can be changed during software execution - then some sort of IConfigService can be implemented, but I would suggest to have a general T GetValue<T>(string configName) in this case.
\$\endgroup\$
  • \$\begingroup\$ Thank you for the feedback, exactly what I was looking for. I will go read and improve the solution. \$\endgroup\$ – Shane Van Wyk Mar 5 '15 at 19:40
  • \$\begingroup\$ Just a point about the logging... Common.Logging, being the 'common interface' that it is, makes you lose a lot of the power you can get from individual logging frameworks, e.g. logging custom fields. In my experience I've found it to be more of a hindrance than a help. \$\endgroup\$ – Gigi Mar 6 '15 at 13:43
  • \$\begingroup\$ @Gigi - Agree. We use Common.Logging when developing frameworks that may be used in other projects, in order not to force specific logging infrastructure \$\endgroup\$ – almaz Mar 6 '15 at 13:56
  • \$\begingroup\$ @almaz about the logging... what do you mean by "All these frameworks do not (and should not) use dependency injection"? Where is the problem of using logging interface and just inject implementation via IoC? I think abstracting logging class is a very good practice and you can see it in almost every IoC tutorial/article. Can you point out what problems did you have with such solution? \$\endgroup\$ – voytek Sep 22 '15 at 18:34
  • \$\begingroup\$ @voytek Look at recommended approach in common.logging, log4net or nlog logging frameworks. All of them suggest to have a logger specific to each class via LogManager.GetLogger (or LogManager.GetCurrentClassLogger) by capturing logger instance in a static readonly field \$\endgroup\$ – almaz Sep 23 '15 at 15:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.