ASP.NET MVC 4 中的瘦控制器






4.75/5 (7投票s)
应用 ASP.NET Web 应用程序的最佳实践。
原始文章 可以在 我的博客 找到
完整的源代码: MSDN 代码
背景
您需要至少了解 C# 和一些 ASP.NET MVC 的背景知识。此外,您需要了解一些基本模式(如果您可以参考 Gang of Four 的书,那就更好了)。
引言
上个月,我发表了一篇关于如何使控制器更瘦的文章。这篇文章也引起了一些人的好奇。两个月后,我完成了 MSDN 代码上的一个示例,现在我真的很高兴向大家展示这个主题的完整版本。现在我想在这个社区中与大家分享。
Rails 社区 总是激发很多好主意。我真的很喜欢这个社区。其中之一是“胖模型和瘦控制器”。我花了很多时间在 ASP.NET MVC 上,真的犯了一些错误,因为我把控制器做得太胖了。这样的控制器真的很脏,并且在未来很难维护。它严重违反了 SRP 原则 和 KISS。但是我们如何在 ASP.NET MVC 中实现这一点呢?在我阅读了“Manning ASP.NET MVC 4 in Action”之后,答案非常清楚。很简单,我们可以将其分离到 ActionResult 中,并尝试在此处实现逻辑和持久性数据。在过去的两年里,我从 Jimmy Bogard 的博客中读到过,但在当时我从未考虑过它。现在说够了。
我刚刚在 ASP.NET MVC 4 上发布了一个示例,该示例在 Visual Studio 2012 上实现。我在这里使用 EF 框架来实现持久层,并且还使用了来自互联网的两个免费模板来为这个示例制作 UI。
在这个示例中,我尝试实现一个简单的杂志网站,该网站管理文章、类别和新闻。目前还没有完全完成,但没问题,因为我只需要向您展示如何使控制器变瘦。我想听到更多关于你的想法。
首先,我正在抽象化基本 ActionResult
类,如下所示
public abstract class MyActionResult : ActionResult, IEnsureNotNull
{
public abstract void EnsureAllInjectInstanceNotNull();
}
public abstract class ActionResultBase<TController> : MyActionResult where TController : Controller
{
protected readonly Expression<Func<TController, ActionResult>> ViewNameExpression;
protected readonly IExConfigurationManager ConfigurationManager;
protected ActionResultBase (Expression<Func<TController, ActionResult>> expr)
: this(DependencyResolver.Current.GetService<IExConfigurationManager>(), expr)
{
}
protected ActionResultBase(
IExConfigurationManager configurationManager,
Expression<Func<TController, ActionResult>> expr)
{
Guard.ArgumentNotNull(expr, "ViewNameExpression");
Guard.ArgumentNotNull(configurationManager, "ConfigurationManager");
ViewNameExpression = expr;
ConfigurationManager = configurationManager;
}
protected ViewResult GetViewResult<TViewModel>(TViewModel viewModel)
{
var m = (MethodCallExpression)ViewNameExpression.Body;
if (m.Method.ReturnType != typeof(ActionResult))
{
throw new ArgumentException("ControllerAction method '" +
m.Method.Name + "' does not return type ActionResult");
}
var result = new ViewResult
{
ViewName = m.Method.Name
};
result.ViewData.Model = viewModel;
return result;
}
public override void ExecuteResult(ControllerContext context)
{
EnsureAllInjectInstanceNotNull();
}
}
我还有一个用于验证所有注入对象的接口。此接口确保我使用 Autofac 容器注入的所有注入对象都不为空。此接口的实现如下
public interface IEnsureNotNull
{
void EnsureAllInjectInstanceNotNull();
}
之后,我只是简单地实现 HomePageViewModelActionResult
类,如下所示
public class HomePageViewModelActionResult<TController> : ActionResultBase<TController> where TController : Controller
{
#region variables & ctors
private readonly ICategoryRepository _categoryRepository;
private readonly IItemRepository _itemRepository;
private readonly int _numOfPage;
public HomePageViewModelActionResult(Expression<Func<TController, ActionResult>> viewNameExpression)
: this(viewNameExpression,
DependencyResolver.Current.GetService<ICategoryRepository>(),
DependencyResolver.Current.GetService<IItemRepository>())
{
}
public HomePageViewModelActionResult(
Expression<Func<TController, ActionResult>> viewNameExpression,
ICategoryRepository categoryRepository,
IItemRepository itemRepository)
: base(viewNameExpression)
{
_categoryRepository = categoryRepository;
_itemRepository = itemRepository;
_numOfPage = ConfigurationManager.GetAppConfigBy("NumOfPage").ToInteger();
}
#endregion
#region implementation
public override void ExecuteResult(ControllerContext context)
{
base.ExecuteResult(context);
var cats = _categoryRepository.GetCategories();
var mainViewModel = new HomePageViewModel();
var headerViewModel = new HeaderViewModel();
var footerViewModel = new FooterViewModel();
var mainPageViewModel = new MainPageViewModel();
headerViewModel.SiteTitle = "Magazine Website";
if (cats != null && cats.Any())
{
headerViewModel.Categories = cats.ToList();
footerViewModel.Categories = cats.ToList();
}
mainPageViewModel.LeftColumn = BindingDataForMainPageLeftColumnViewModel();
mainPageViewModel.RightColumn = BindingDataForMainPageRightColumnViewModel();
mainViewModel.Header = headerViewModel;
mainViewModel.DashBoard = new DashboardViewModel();
mainViewModel.Footer = footerViewModel;
mainViewModel.MainPage = mainPageViewModel;
GetViewResult(mainViewModel).ExecuteResult(context);
}
public override void EnsureAllInjectInstanceNotNull()
{
Guard.ArgumentNotNull(_categoryRepository, "CategoryRepository");
Guard.ArgumentNotNull(_itemRepository, "ItemRepository");
Guard.ArgumentMustMoreThanZero(_numOfPage, "NumOfPage");
}
#endregion
#region private functions
private MainPageRightColumnViewModel BindingDataForMainPageRightColumnViewModel()
{
var mainPageRightCol = new MainPageRightColumnViewModel();
mainPageRightCol.LatestNews = _itemRepository.GetNewestItem(_numOfPage).ToList();
mainPageRightCol.MostViews = _itemRepository.GetMostViews(_numOfPage).ToList();
return mainPageRightCol;
}
private MainPageLeftColumnViewModel BindingDataForMainPageLeftColumnViewModel()
{
var mainPageLeftCol = new MainPageLeftColumnViewModel();
var items = _itemRepository.GetNewestItem(_numOfPage);
if (items != null && items.Any())
{
var firstItem = items.First();
if (firstItem == null)
throw new NoNullAllowedException("First Item".ToNotNullErrorMessage());
if (firstItem.ItemContent == null)
throw new NoNullAllowedException("First ItemContent".ToNotNullErrorMessage());
mainPageLeftCol.FirstItem = firstItem;
if (items.Count() > 1)
{
mainPageLeftCol.RemainItems = items.Where(x => x.ItemContent != null &&
x.Id != mainPageLeftCol.FirstItem.Id).ToList();
}
}
return mainPageLeftCol;
}
#endregion
}
最后一步,我进入 HomeController 并添加一些代码,如下所示
[Authorize]
public class HomeController : BaseController
{
[AllowAnonymous]
public ActionResult Index()
{
return new HomePageViewModelActionResult<HomeController>(x=>x.Index());
}
[AllowAnonymous]
public ActionResult Details(int id)
{
return new DetailsViewModelActionResult<HomeController>(x => x.Details(id), id);
}
[AllowAnonymous]
public ActionResult Category(int id)
{
return new CategoryViewModelActionResult<HomeController>(x => x.Category(id), id);
}
}
正如你所看到的,控制器中的代码非常瘦,我将所有逻辑都移动到了自定义 ActionResult
类中。有些人说,它只是将代码从控制器中移出并放入另一个类中,因此仍然难以维护。看起来它只是将复杂的代码从一个地方移动到另一个地方。但是,如果您仔细观察并思考一下,您必须找出您是否有用于处理与 HttpContext 相关的所有逻辑的代码,或者类似的东西。您可以在 Controller 上执行此操作,并尝试将另一个逻辑(例如处理业务需求、持久性数据等)委托给自定义 ActionResult
类。
关注点
这非常令人兴奋,因为我们可以将复杂的代码分离到 ActionResult
并尝试在那里进行业务逻辑。
这使得控制器非常瘦,这意味着我们可以在控制器上非常容易地进行单元测试。并且在 ActionResult
代码上,它只关注应用程序业务。我们可以很容易地对它们两个进行单元测试。
历史
1.0 草稿版本。