基于MVC、EF和Fluent Validation的简单三层应用
在本文中,我将尝试描述我对基于.NET技术创建简单三层应用程序的看法。
引言
有很多关于如何在EF之上构建UoW(工作单元)与存储库,或者使用Fluent Validator代替基于Data Annotation的模型验证的文章。但我没有找到提供将所有这些内容结合在一起所需的简单步骤的文章。所以,让我们开始吧,尽量保持简单。
背景
如上所述,我将创建一个基于ASP.NET MVC、UoW + Repository、Entity Framework、Fluent validation和MsSQL的简单三层解决方案。本文将对那些
- 不想直接从控制器使用DAL(存储库和工作单元)的人有用。
- 希望使用Fluent Validation作为一种替代且更灵活的数据验证方式的人。
- 不想让模型和验证逻辑紧密耦合的人。
- 希望使用替代方式触发验证过程,而不是直接调用它或将其委托给EF的人。
- 希望使用UoW + EF实现事务管理的人。
Using the Code
请注意:代码部分不包含所有类的描述。主要目的是强调对架构具有重要价值的类。
初始步骤
我将使用Visual Studio 2013创建一个没有身份验证的MVC 5.0应用程序。我还将添加用于存储业务、验证、数据访问逻辑的项目。
CountryCinizens
CountryCinizens
CountryCinizens.Services
// 包含验证逻辑。CountryCinizens.DAL
数据库
将为以下项目创建两个表
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
数据访问层 (DAL)
数据访问层将基于Entity Framework 6.0之上的Unit of Work (UoF) + Repositories。没什么特别的。我从网上获取了代码。
- http://www.asp.net/mvc/overview/older-versions/getting-started-with-ef-5-using-mvc-4/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application
- http://blog.longle.net/2013/05/11/genericizing-the-unit-of-work-pattern-repository-pattern-with-entity-framework-in-mvc/
验证层
我决定使用Fluent Validation代替基于Data Annotation的模型验证,原因如下:
- 对我来说,它比标准方法灵活得多。
- 我将采用的方法允许我们根据正在执行的业务流程动态添加验证器。
- 易于本地化验证消息。
- 易于创建自定义验证器或基于“RuleSet”配置验证。
- 可以使用单元测试来验证验证逻辑。
下面是一些验证类的示例。
验证器
用于验证一条验证规则的类。示例:Country
验证器用于验证Name
和Capital
属性。
public class CountryValidator : AbstractValidator<Country>
{
public CountryValidator()
{
RuleFor(c => c.Name)
.NotEmpty();
RuleFor(c => c.Capital)
.NotEmpty();
}
}
Fluent Validation框架允许轻松创建自定义验证。示例:一个检查是否存在引用该国家的用户的验证器,
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
存储一组验证器(验证规则)并实现Command模式来执行所有这些验证的类。
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
使用validation command实例来验证参数(实体)的类。
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);
}
}
}
}
触发验证
现在是时候将服务层、DAL和验证结合在一起了。不同的作者推荐不同的执行验证逻辑的位置。我确信的一点是,我不喜欢将触发验证过程委托给EntityFramework.SaveChanges
方法,因为那样我们就与EF紧密耦合了,或者直接从存储库调用它,因为它们不应该了解验证,它们的主要职责是数据持久化。我将使用代理存储库,在调用存储库实例之前触发验证逻辑。
SafetyRepositoryProxy
包装存储库并负责使用ValidationCommandExecutor
实例触发验证逻辑的类。
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
负责创建存储库代理实例的类。
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;
}
}
创建country
实例的验证命令的代码片段。
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);
事务管理
实现事务管理最简单的方法是使用TransctionScope
。但这种方法与EF不完全一致,EF提供了SaveChanges
方法来在单个事务中执行批量数据库操作。我将通过添加两个新函数来扩展IUnitOfWork
接口:
BeginScope
- 调用此方法将指示不可分割命令的开始。NotifyAboutError
- 通知UoW在代码执行过程中出现的错误。EndScope
- 调用此方法将指示不可分割命令的结束。如果没有其他人在之前打开scope,则事务将被完成/回滚。否则,事务将在上层完成/回滚。
UnitOfWork
事务scope实现如下:
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();
}
}
}
}
示例。Country
服务将实现创建具有其公民的国家并在单个事务中完成的业务流程。服务名称为“CreateWithUsers
”。
业务逻辑层(服务)
服务层将包含业务领域逻辑。从技术角度来看,这一层将与DAL交互,管理事务,执行数据验证。
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));
}
}
引入Unity Container
将引入Unity Container来简化对象创建过程并减少代码行数。
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)));
}
}
从MVC控制器使用Country Service的示例
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");
}
}
就这些了。
我在对象图创建过程中做了一些技巧。存储库必须通过从UoW传递IDBContext
对象来解析。但这是一个基于单个服务和共享DbConext
的简单示例,所以我决定保持代码原样。
我不喜欢使用try
/catch
/finally
并在服务层将异常重新抛到上层。这可以通过使用“using
”语句来解决。
结论
在本文中,我尝试简要描述了基于.NET技术创建三层应用程序的简单步骤。解决方案源代码可以从文章开头提供的链接下载。