zoukankan      html  css  js  c++  java
  • 使用ModelBinder绑定IPrincipal (User)简化ASP.NET MVC单元测试

    I am working on some code like this:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    [Authorize]
    public ActionResult Edit(int id) {
     
        Dinner dinner = dinnerRepository.FindDinner(id);
     
        if (dinner.HostedBy != User.Identity.Name)
            return View("InvalidOwner");
     
        var viewModel = new DinnerFormViewModel {
            Dinner = dinner,
            Countries = new SelectList(PhoneValidator.Countries, dinner.Country)
        };
     
        return View(viewModel);
    }

    It's pretty straight forward, but this Controller knows too much. It's reaching into implicit parameters. The id was passed in, but the User is actually a property of the Controller base class and ultimately requires an HttpContext. Having this method "know" about the User object, and worse yet, having the User object go reaching into HttpContext.Current makes this hard to test.

    I'd like to have the convenience of passing in the User (actually an IPrincipal interface) when I want to test, but when I'm running the app, I'd like to have the IPrincipal get passed into my method automatically. Enter the Model Binder. I need to teach ASP.NET MVC what to do when it sees a type as a parameter.

    This quickie model binder is now responsible for one thing - it knows how to reach down into the HttpContext and get the current User (IPrincipal). It has one single responsibility.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class IPrincipalModelBinder : IModelBinder
    {
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            if (controllerContext == null) {
                throw new ArgumentNullException("controllerContext");
            }
            if (bindingContext == null) {
                throw new ArgumentNullException("bindingContext");
            }
            IPrincipal p = controllerContext.HttpContext.User;
            return p;
        }
    }

    Now I can release the Controller from the emotional baggage of knowing too much about the User object. It can just have that passed in automatically by the framework. I just need to register the binder to tell folks about it. I can either do it on a one-off basis and put an attribute on this one method parameter:

    1
    2
    3
    4
    public ActionResult Edit(int id,                       
                             [ModelBinder(typeof(IPrincipalModelBinder))]                       
                             IPrincipal user)
    {...}

    But even better, I can just tell the whole application once in the global.asax:

    1
    2
    3
    4
    void Application_Start() {
        RegisterRoutes(RouteTable.Routes); //unrelated, don't sweat this line.
        ModelBinders.Binders[typeof(IPrincipal)] = new IPrincipalModelBinder();
    }

    Now that ASP.NET MVC knows what to do when it see an IPrincipal as a method parameter, my method gets nicer.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    [Authorize]
    public ActionResult Edit(int id, IPrincipal user) {
     
        Dinner dinner = dinnerRepository.FindDinner(id);
     
        if (dinner.HostedBy != user.Identity.Name)
            return View("InvalidOwner");
     
        var viewModel = new DinnerFormViewModel {
            Dinner = dinner,
            Countries = new SelectList(PhoneValidator.Countries, dinner.Country)
        };
     
        return View(viewModel);
    }

    Now I can test my controller more easily by passing in fake users. No need for mocking in this case!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    [TestMethod]
    public void EditAllowsUsersToEditDinnersTheyOwn()
    {
        // Arrange
        DinnersController controller = new DinnersController(new TestDinnerRespository());
     
        // Act
        IPrincipal FakeUser = new GenericPrincipal(new GenericIdentity("Scott","Forms"),null);
        ViewResult result = controller.Edit(4, FakeUser) as ViewResult;
     
        // Yada yada yada assert etc etc etc
        Assert.IsTrue(result.ViewName != "InvalidOwner");
    }

    Fun stuff.

    UPDATE: Phil had an interesting idea. He said, why not make method overloads, one for testing and one for without. I can see how this might be controversial, but it's very pragmatic.

    1
    2
    3
    4
    5
    [Authorize]
    public ActionResult Edit(int id)
    {
        return Edit(id, User); //This one uses HttpContext
    }

    You'd use this one as before at runtime, and call the overload that takes the IPrincipal explicitly for testing.

    Yes, I realize I could use an IoC container for this also.

  • 相关阅读:
    MSN、易趣、大旗被挂马 用户浏览后感染机器狗病毒 狼人:
    世界最大漏洞数据库发布最新研究报告 狼人:
    五角大楼最昂贵武器发展项目遭到黑客攻击 狼人:
    RSA呼吁厂商“创造性协作” 共同反击网络威胁 狼人:
    RSA2009:云计算服务如何保证安全? 狼人:
    黑客工具可将恶意软件隐藏于.Net框架 狼人:
    RSA安全大会将亮相25款热门安全产品 狼人:
    目录访问共享C#怎么访问共享目录
    代码下行Jquery结合Ajax和Web服务使用三层架构实现无刷新分页
    输出次数HDU2192:MagicBuilding
  • 原文地址:https://www.cnblogs.com/hyl8218/p/2085754.html
Copyright © 2011-2022 走看看