65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET MVC 4 中的瘦控制器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (7投票s)

2012 年 11 月 26 日

CPOL

3分钟阅读

viewsIcon

25183

应用 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 草稿版本。

© . All rights reserved.