Click here to Skip to main content
15,063,859 members
Articles / Web Development / HTML
Article
Posted 23 Oct 2015

Stats

13.4K views
493 downloads
15 bookmarked

Simple 3-layer App Based on MVC, EF and Fluent Validation

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
23 Oct 2015CPOL4 min read
In this article, I will try to describe my vision on creation of simple 3-layer application based on .NET technologies.

Introduction

There are many articles on how to build UoW with repositories over EF, or Fluent Validators instead of Model validation based on Data Annotation approach. But I've not found articles which provide simple steps needed to combine all these things together. So let's start and do things as simple as possible.

Background

As was mentioned above, I will create a simple 3-tier solution based on ASP.NET MVC, UoW + Repository, Entity Framework, Fluent validation and MsSQL. This article will be useful for those who:

  • don't want to use DAL (repositories and UoW) directly from controllers.
  • want to use Fluent Validation as an alternative and more flexible way for data validation
    • don't want tightly coupled model and validation logic
  • want to use alternative way for triggering validation process instead of calling it directly or delegate this task to EF.
  • want to implement transactions management using UoW + EF

Using the Code

Please note: Code part does not contain description for all classes. The main purpose is to emphasize classes which have a major value for architecture.

Initial Step

I will use Visual Studio 2013 to create MVC 5.0 application without authentication. I will also add projects for storing business, validation, data access logic.

CountryCinizens

  • CountryCinizens
  • CountryCinizens.Services // Including validation logic.
  • CountryCinizens.DAL

Database

Two tables will be created for the following project:

SQL
CREATE TABLE [dbo].[Country](
    [CountryId] [int] IDENTITY(1,1) NOT NULL,
    [Name] [nvarchar](255) NOT NULL,
    [IndependenceDay] [datetime] NOT NULL,
    [Capital] [nvarchar](255) NULL,
PRIMARY KEY CLUSTERED 
(
    [CountryId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, _
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

CREATE TABLE [dbo].[User](
    [UserId] [int] IDENTITY(1,1) NOT NULL,
    [FirstName] [nvarchar](255) NOT NULL,
    [LastName] [nvarchar](255) NOT NULL,
    [EMail] [nvarchar](255) NOT NULL,
    [Phone] [nvarchar](100) NULL,
    [CountryId] [int] NOT NULL,
PRIMARY KEY CLUSTERED 
(
    [UserId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, _
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

ALTER TABLE [dbo].[User]  WITH CHECK ADD  CONSTRAINT [FK_User_Country] FOREIGN KEY([CountryId])
REFERENCES [dbo].[Country] ([CountryId])
GO

Data Access Layer (DAL)

The data access tier will be based on Unit of Work (UoF) + Repositories over Entity Framework 6.0. Nothing special there. I took the code from the internet.

Validation Layer

I decided to use Fluent Validation instead of Model validation based on Data Annotation because of the following reasons:

  • As for me, it is much more flexible than the standard approach
    • Approach I am going to use will allow us to add validators dynamically depending on the business flow which is being executed
  • It is easy to localize validation messages
  • It is easy to create custom validator or configure validation based on "RuleSet"
  • Validation logic can be verified using UnitTests

Some of the validation classes below.

Validator

The class used to verify one validation rule. Example: Country validator used to verify Name and Capital properties.

C#
public class CountryValidator : AbstractValidator<Country>
{
    public CountryValidator()
    {
        RuleFor(c => c.Name)
            .NotEmpty();

        RuleFor(c => c.Capital)
            .NotEmpty();
    }
}

Fluent Validation framework allows to easily create custom validation. Example: validator which checks if there are users which refer to the country,

C#
public class UserToCountryReferenceValidator: AbstractValidator<Country>
{
    IRepository<User> _userRepository; 

    public UserToCountryReferenceValidator(IRepository<User> userRepository)
    {
        _userRepository = userRepository;

        Custom(entity =>
        {
            ValidationFailure result = null;
            if (_userRepository.QueryBuilder.Filter
            	(u => u.CountryId == entity.CountryId).List().Count() > 0)
            {
                result = new ValidationFailure
                ("CountryId", "The company can't be deleted because users assigned to it.");
            }
            return result;
        }); 
   }
}  

ValidationCommand

The class which stores set of validators (validation rules) and implements Command pattern to execute all of them.

C#
public class ValidationCommand
{
    private object _entityToVerify;
    private IEnumerable<IValidator> _validators;
    private IEntityValidators _entityValidators;

    public ValidationCommand(IEntityValidators entityValidators, params IValidator[] validators)
    {
        this._entityValidators = entityValidators;
        this._validators = validators;
    }

    public ValidationResult Execute()
    {
        var errorsFromOtherValidators = _validators.SelectMany(x => x.Validate(_entityToVerify).Errors);
        return new ValidationResult(errorsFromOtherValidators);
    }

    public void BindTo(object entity)
    {
        this._entityToVerify = entity;
        this._entityValidators.Add(entity.GetHashCode(), this); 
    }
}

ValidationCommandExecutor

The class which uses validation command instance to validate parameters (entities).

C#
public class ValidationCommandExecutor : IValidationCommandExecutor
{
    private IEntityValidators _entityValidators;

    public ValidationCommandExecutor(IEntityValidators entityValidators)
    {
        this._entityValidators = entityValidators;
    }

    public void Process(object[] entities)
    {
        IList<ValidationFailure> validationFailures = new List<ValidationFailure>();
        foreach (var entity in entities)
        {
            ValidationCommand val = this._entityValidators.Get(entity.GetHashCode());
            if (val != null)
            {
                ValidationResult vr = val.Execute();
                foreach (ValidationFailure error in vr.Errors)
                {
                    validationFailures.Add(error);    
                }
            }
            if (validationFailures.Count > 0)
            {
                throw new ValidationException(validationFailures);
            }
        }
    }
}

Triggering Validation

Now it's time to combine service tier, DAL and validation together. Different authors recommend different places to execute validation logic. One thing I'm sure exactly that I don't like to delegate triggering validation process to EntityFramework.SaveChanges method because that way we tightly coupled with EF, or to call it directly from repositories because they should know nothing about validation and their main responsibility is data persistence. I will user proxy repository for triggering validation logic just before calling repository instance.

SafetyRepositoryProxy

The class which wraps repository and is responsible for triggering validation logic using ValidationCommandExecutor instance.

C#
public class SafetyRepositoryProxy<T> : RealProxy
{
    private const string INSERT = "Insert";
    private const string UPDATE = "Update";
    private const string DELETE = "Delete";

    private readonly T _decoratedRepository;
    private readonly IValidationCommandExecutor _valCommExecutor;

    public SafetyRepositoryProxy(T decorated, IValidationCommandExecutor valCommExecutor)
        : base(typeof(T))
    {
        this._decoratedRepository = decorated;
        this._valCommExecutor = valCommExecutor;
    }

    public override IMessage Invoke(IMessage msg)
    {
        var methodCall = msg as IMethodCallMessage;
        var methodInfo = methodCall.MethodBase as MethodInfo;

        if (isValidationNeeded(methodCall.MethodName))
        {
            this._valCommExecutor.Process(methodCall.Args);
        }
        try
        {
            var result = methodInfo.Invoke(this._decoratedRepository, methodCall.InArgs);
            return new ReturnMessage
                (result, null, 0, methodCall.LogicalCallContext, methodCall);
        }
        catch (Exception e)
        {
            return new ReturnMessage(e, methodCall);
        }
    }

    private bool isValidationNeeded(string methodName)
    {
        return methodName.Equals(INSERT) || methodName.Equals(UPDATE) || 
            methodName.Equals(DELETE);
    }
}

SafetyRepositoryFactory

The class responsible for creation of repository proxy instance.

C#
public class SafetyRepositoryFactory : IRepositoryFactory
{
    private IUnityContainer _container;

    public SafetyRepositoryFactory(IUnityContainer container)
    {
        _container = container;
    }

    public RT Create<RT>() where RT : class
    {
        RT repository = _container.Resolve<RT>();
        var dynamicProxy = new SafetyRepositoryProxy<RT>
				(repository, _container.Resolve<IValidationCommandExecutor>());
        return dynamicProxy.GetTransparentProxy() as RT;
    }
}

Piece of code which creates validation command for country instance.

C#
private IEntityValidators _entityValidators; // will be initialized on upper layer level.
...
Country country = new Country();
country.Name = "Ukraine";
country.IndependenceDay = new DateTime(1991, 8, 24);
country.Capital = "Kyiv";

var countValComm = new ValidationCommand(this._entityValidators,
   new CountryValidator());

countryValComm.BindTo(country);

Transactions Management

The simplest way to implement transactions management is to use TransctionScope. But this approach is not fully lined up with EF which offers SaveChanges method to perform bulk database operations in a single transaction. I will extend IUnitOfWork interface by adding two new functions:

  • BeginScope - calling this method will indicate the start of indivisible command
  • NotifyAboutError - notify UoW about error that has appeared during code execution
  • EndScope - calling this method will indicate the end of indivisible command. Transaction will be completed/rolled back if no one else has opened scope before. Otherwise transaction will be completed/rolled back on upper level.

UnitOfWork transaction scope implementation is as given below:

C#
public class UnitOfWork : IUnitOfWork
{
    private readonly IDbContext _context;
    private int _scopeInitializationCounter;
    private bool _rollbackChanges;

    public UnitOfWork(IDbContext context)
    {
        this._context = context;
        this._scopeInitializationCounter = 0;
    }

    public void Save()
    {
        this._context.SaveChanges();
    }

    public void BeginScope()
    {
        this._scopeInitializationCounter++;
    }

    public void NotifyAboutError()
    {
        this._rollbackChanges = true;
    }

    public void FinalizeScope()
    {
        this._scopeInitializationCounter--;
        if (this._scopeInitializationCounter == 0)
        {
            if (this._rollbackChanges)
            {
                this._rollbackChanges = false;
                this._context.DiscardChanges();  
            }
            else
            {
               this.Save();
            }
        }
    }
}

Example. Country service will implement business flow for creating country with its citizens in a single transaction. Service name "CreateWithUsers".

Business Logic Layer (Service)

Service layer will hold business domain logic. From technical point of view, this layer will interact with DAL, manage transactions, perform data validation.

C#
public class CountryService : ICountryService
{
    private IUnitOfWork _uow;
    private ICountryRepository _countryRepository;
    private IRepository<User> _userRepository;
    private IEntityValidators _entityValidators;

    public CountryService(IUnitOfWork uow, 
        IRepositoryFactory repositoryFactory, IEntityValidators entityValidators)
    {
        this._uow = uow;
        this._countryRepository = repositoryFactory.Create<ICountryRepository>(); 
        this._userRepository = repositoryFactory.Create<IRepository<User>>();  
        this._entityValidators = entityValidators;
    }

    public IEnumerable<Country> List()
    {
        return this._countryRepository.QueryBuilder.List();  
    }

    public Country FindById(int id)
    {
        return this._countryRepository.FindById(id);  
    }

    public Country FindByName(string name)
    {
        return this._countryRepository.QueryBuilder.Filter
            (c => c.Name == name).List().FirstOrDefault();   
    }

    public Country Create(Country country)
    {
        Country newCountry = null;
        try
        {
            this._uow.BeginScope();
            newCountry = new Country();
            newCountry.Name = country.Name;
            newCountry.IndependenceDay = country.IndependenceDay;
            newCountry.Capital = country.Capital;

            var countryValComm = new ValidationCommand(this._entityValidators,
                new CountryValidator(),
                new UniqueCountryValidator(this._countryRepository));

            countryValComm.BindTo(newCountry);

            this._countryRepository.Insert(newCountry);
        }
        catch (CountryCitizens.Services.Validators.ValidationException ve)
        {
            this._uow.NotifyAboutError();
            throw ve;
        }
        finally
        {
            this._uow.EndScope();
        }
        return newCountry;
    }

    public Country CreateWithUsers(Country country, IList<User> users)
    {
        Country newCountry = null;
        try { 
            this._uow.BeginScope();  
            newCountry = this.Create(country);

            foreach(var u in users) { 
                User newUser = new User(); 
                newUser.Country = newCountry;
                newUser.FirstName = u.FirstName;
                newUser.LastName = u.LastName;
                newUser.EMail = u.EMail;

                var userValComm = new ValidationCommand(
                    this._entityValidators,
                    new UserValidator());
                userValComm.BindTo(newUser);

                this._userRepository.Insert(newUser);
            }
        }
        catch (Exception e)
        {
            this._uow.NotifyAboutError();
            throw e;
        }
        finally
        {
            this._uow.EndScope();
        }
        return newCountry;
    }

    public Country Edit(Country country)
    {
        Country originalCountry = null;
        try
        {
            this._uow.BeginScope();

            originalCountry = this._countryRepository.FindById(country.CountryId);
            originalCountry.Name = country.Name;
            originalCountry.IndependenceDay = country.IndependenceDay;
            originalCountry.Capital = country.Capital;

            var countryValComm = new ValidationCommand(
                this._entityValidators,
              new CountryValidator(),
              new UniqueCountryValidator(this._countryRepository));

            countryValComm.BindTo(originalCountry);

            this._countryRepository.Update(originalCountry);
        }
        catch (Exception e)
        {
            this._uow.NotifyAboutError();
            throw e;
        }
        finally
        {
            this._uow.EndScope();
        }
        return originalCountry;
    }

    public void Delete(Country country)
    {
        try 
        {
            this._uow.BeginScope();  

            ValidationCommand coyntryValComm = new ValidationCommand(
                this._entityValidators,
                new UserToCountryReferenceValidator(this._userRepository));

            coyntryValComm.BindTo(country); 

            this._countryRepository.Delete(country);
        }
        finally
        {
            this._uow.EndScope();
        }
    }

    public void Delete(int countryId)
    {
        this.Delete(this._countryRepository.FindById(countryId)); 
    }
}

Involve Unity Container

Unity Container will be involved to simplify the process of objects creation and decrease number of code lines.

C#
public class UnityConfig
{
    public static void RegisterTypes(IUnityContainer container)
    {
        container.RegisterType<IDbContext, 
        CountryCitizensEntities>(new PerRequestLifetimeManager());
        container.RegisterType<IUnitOfWork, UnitOfWork>(new PerRequestLifetimeManager());
  
        container.RegisterType<ICountryRepository, 
        CountryRepository>(new InjectionConstructor(typeof(IDbContext)));
        container.RegisterType(typeof(IRepository<>), typeof(Repository<>));
        container.RegisterType<IRepositoryFactory, 
        SafetyRepositoryFactory>(new PerRequestLifetimeManager(), new InjectionConstructor(container));

        container.RegisterType<IEntityValidators, EntityValidators>(new PerRequestLifetimeManager());
        container.RegisterType<IValidationCommandExecutor, 
        ValidationCommandExecutor>(new InjectionConstructor(typeof(IEntityValidators)));
        
        container.RegisterType<ICountryService, 
        CountryService>(new InjectionConstructor(typeof(IUnitOfWork), 
        typeof(IRepositoryFactory), typeof(IEntityValidators))); 
    }
}

An Example of Using Country Service from MVC Controller

C#
public class CountryController : Controller
{
    private CountryService _countryService;

    public CountryController(CountryService countryService)
    {
        this._countryService = countryService;
    }

    public ActionResult Index()
    {
        var model = this._countryService.List();
        return View(model);
    }
    
    [HttpPost]
    public ActionResult Create(Country c)
    {
        try
        {
            var createdCountry = this._countryService.Create(c);
        }
        catch (ValidationException)
        {
            // get and display validation errors
            return View(c);
        }
        return RedirectToAction("Index"); 
    }
}

That is all.

I did some trick during object graph creation. The repository must be resolved by passing the IDBContext object from UoW. But this is a simple example based on single service and shared DbConext, so I decided to leave the code as is.

What I don't like is using try/catch/finally and re-throwing exception from service layer to upper layer. This could be resolved by using "using" statement.

Conclusion

In this article, I tried to briefly describe simple steps for creation of 3-layer app based on .NET technologies. Solution source code can be downloaded from the link provided at the beginning of an article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

alex_lviv
Ukraine Ukraine
No Biography provided

Comments and Discussions

 
-- There are no messages in this forum --