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:
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.
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,
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.
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).
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.
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.
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.
private IEntityValidators _entityValidators;
...
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:
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.
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.
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
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)
{
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.