【问题标题】:Should my MVC controller really know about JSON?我的 MVC 控制器真的应该知道 JSON 吗?
【发布时间】:2010-10-03 16:47:39
【问题描述】:

JsonResult 类是一种通过 AJAX 将 Json 作为操作返回给客户端的非常有用的方法。

public JsonResult JoinMailingList(string txtEmail)
{
        // ...

       return new JsonResult()
       {
           Data = new { foo = "123", success = true }
       };
}

但是(至少根据我的第一印象)这确实不是一个很好的关注点分离。

  • 单元测试方法更难编写,因为它们没有很好的强类型数据来测试并且必须知道如何解释 Json。
  • 未来不通过 HTTP(或任何涉及序列化的远程协议)的其他视图更难被“插入”,因为在这种情况下不需要序列化和反序列化响应。
  • 如果您有两个不同的地方需要该操作的结果怎么办?一个想要 Json,另一个想要 XML,或者可能是一个完全 or partially 呈现的视图。

我想知道为什么对象和 Json 之间的转换不是通过属性以声明方式实现的。在下面的代码中,您实际上是在告诉 MVC this method is convertible to Json,然后如果它是从 AJAX 客户端调用的,则会检查内部执行的 new JsonResult() 转换属性。

单元测试可以只取action结果(ObjectActionResult),拉出强类型的Foo

[JsonConvertible]
public ActionResult JoinMailingList(string txtEmail)
{
        // ...

       return new ObjectActionResult()
       {
           Data = new Foo(123, true)
       };
}

我只是好奇人们的想法和任何其他可以遵循的模式。

这些也只是我最初的观察——这可能不是一个理想的设计可能还有更多的原因(可能还有很多原因为什么它是一个完全可以接受和实用的设计!)我今晚只是感觉理论和魔鬼的拥护者。

 * 免责声明:我什至还没有开始考虑该属性将如何实现或它可能具有的副作用或反作用等。

【问题讨论】:

  • 就是我的想法。鉴于权力,我会为这个问题投票 5 次!
  • 这是 MVC3 中的一个有效问题。但是,在较新的 MVC4 中,任何 ApiController 操作方法都会自动评估 Accept HTTP 标头并相应地返回 JSON 或 XML。 action 方法只需要返回一个视图模型,这一切都是自动发生的,这使得单元测试再次变得容易!

标签: asp.net-mvc json design-patterns


【解决方案1】:

我认为你正在为无所事事而烦恼。那么如果控制器在其公共接口中知道 JSON 怎么办?

曾经有人告诉我:“让你的代码通用,不要让你的应用程序通用。”

您正在这里编写应用程序控制器。应用程序控制器(其职责是在模型和视图之间进行缓和并调用模型中的更改)了解某个视图(JSON、HTML、PList、XML、YAML)是可以的。

在我自己的项目中,我通常有这样的:

interface IFormatter {
    ActionResult Format(object o);
}
class HtmlFormatter : IFormatter {
    // ...
}
class JsonFormatter : IFormatter {
    // ...
}
class PlistFormatter : IFormatter {
    // ...
}
class XmlFormatter : IFormatter {
    // ...
}

基本上是“格式化程序”,它接受对象并赋予它们不同的表示形式。如果HtmlFormatters 的对象实现了IEnumerable,那么HtmlFormatters 甚至可以输出表格。

现在返回数据(或可以使用HtmlFormatters 生成网站部分内容)的控制器采用“格式”参数:

public ActionResult JoinMailingList(string txtEmail, string format) {
    // ...
    return Formatter.For(format).Format(
        new { foo = "123", success = true }
    );
}

您可以为单元测试添加“对象”格式化程序:

class ObjectFormatter : IFormatter {
    ActionResult Format(object o) {
        return new ObjectActionResult() {
            Data = o
        };
    }
}

使用这种方法,您的任何查询/操作/过程/ajax 调用,无论您想调用什么,都可以以多种格式输出。

【讨论】:

  • @frank 更多的是我不希望控制器对 JSON 了解太多。即使使用我建议的解决方案,控件仍然“了解 JSON”(至少是 JsonAttribute)。不过,看看您如何解决相同的基本问题很有趣。谢谢。正如我提到的单元测试也是一个驱动问题
  • @frank 你不同意,尽管框架级解决方案会更优雅。他们基本上可以采用您自己的格式化程序模式,其中“格式”参数是“秘密”框架参数。
  • 我已经厌倦了等待框架为我提供让我的开发生活更美好的东西。这些格式化程序允许我查询我的数据库并获得很好的结果集。它们的实现短小精悍,并且对控制器的干扰很小。
  • 查看更新。不要过度架构或等待其他人过度架构。
  • @frank 同意。我刚来 MVC 有点晚(5 天前) - 在最佳实践上追赶一点。对于 V1 这样的功能显然为时已晚,但如果有足够多的人同意我的观点,我会建议将其作为增强功能。似乎它很容易对现有代码产生零影响。
【解决方案2】:

我通常尽量不担心。 Asp.Net MVC 足以将关注点分离以将泄漏降至最低。不过你是对的;测试时有一点障碍。

这是我使用的一个测试助手,效果很好:

protected static Dictionary<string, string> GetJsonProps(JsonResult result)
{
    var properties = new Dictionary<string, string>();
    if (result != null && result.Data != null)
    {
        object o = result.Data;
        foreach (PropertyDescriptor prop in TypeDescriptor.GetProperties(o))
            properties.Add(prop.Name, prop.GetValue(o) as string);
    }
    return properties;
}

您可以使用 Request.IsAjaxRequest() 扩展方法返回不同的 ActionResult 类型:

if (this.Request != null && this.Request.IsAjaxRequest())
    return Json(new { Message = "Success" });
else
    return RedirectToAction("Some Action");

注意:您需要 Request != null 才不会破坏您的测试。

【讨论】:

  • 谢谢 - 当然 Request.IsMvcAjaxRequest 现在更名为 IsAjaxRequest()
【解决方案3】:

我并不像以前那样担心返回 JSON。 AJAX 的性质似乎是,您想要在 Javascript 中处理的消息仅适用于该 AJAX 情况。 AJAX 对性能的需求只需要以某种方式影响代码。您可能不想将相同的数据返回给不同的客户端。

我注意到一些关于测试 JSonResult 的事情(我还没有为我的应用编写任何测试):

1) 当您从测试方法“接收”的操作方法返回 JSonResult 时,您仍然可以访问原始数据对象。起初这对我来说并不明显(尽管有些明显)。 Rob 在上面(或者可能在下面!)的答案使用这个事实来获取 Data 参数并从中创建一个字典。如果 Data 是已知类型,那么您当然可以将其转换为该类型。

就我个人而言,我只通过 AJAX 返回非常非常简单的消息,没有任何结构。我想出了一个扩展方法,如果你只有一个从匿名类型构造的简单消息,它可能对测试很有用。如果您的对象有多个“级别” - 无论如何,您最好创建一个实际的类来表示 JSON 对象,在这种情况下,您只需将 jsonResult.Data 转换为该类型。

先示例使用:

动作方法:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult ContactUsForm(FormCollection formData){

     // process formData ...

     var result = new JsonResult()
     {
          Data = new { success = true, message = "Thank you " + firstName }
     };

     return result;
}

单元测试:

var result = controller.ContactUsForm(formsData);
if (result is JSonResult) {

     var json = result as JsonResult;
     bool success = json.GetProperty<bool>("success");
     string message = json.GetProperty<string>("message");

     // validate message and success are as expected
}

然后您可以在测试中运行断言或任何您想要的东西。另外,如果类型不符合预期,扩展方法会抛出异常。

扩展方法:

public static TSource GetProperty<TSource>(this JsonResult json, string propertyName) 
{
    if (propertyName == null) 
    {
        throw new ArgumentNullException("propertyName");
    }

    if (json.Data == null)
    {
        throw new ArgumentNullException("JsonResult.Data"); // what exception should this be?
    }

    // reflection time!
    var propertyInfo = json.Data.GetType().GetProperty(propertyName);

    if (propertyInfo == null) {
        throw new ArgumentException("The property '" + propertyName + "' does not exist on class '" + json.Data.GetType() + "'");
    }

    if (propertyInfo.PropertyType != typeof(TSource))
    {
        throw new ArgumentException("The property '" + propertyName + "' was found on class '" + json.Data.GetType() + "' but was not of expected type '" + typeof(TSource).ToString());
    }

    var reflectedValue = (TSource) propertyInfo.GetValue(json.Data, null);
    return reflectedValue;
}

【讨论】:

    【解决方案4】:

    我认为您有一个正确的观点 - 为什么不将“接受的响应类型与生成的响应类型解析”委托给它实际所属的某个地方?

    这让我想起了 Jeremy Miller 关于如何制作 ASP.NET MVC 应用程序的观点之一:“Opinions” on the ASP.NET MVC

    在他们的应用程序中,所有控制器操作都有一个简洁的界面 - 一些视图模型对象进入,另一个视图模型对象离开。

    【讨论】:

      【解决方案5】:

      我不确定这实际上是一个多大的问题,但在 ASP.NET MVC 中遵循的“替代模式”将是编写一个 JSON ViewEngine。这实际上并没有那么困难,因为框架中内置的 JSON 功能将为您完成大部分繁重的工作。

      我确实认为这会是一个更好的设计,但我不确定它是否值得反对“官方”实现 JSON 的方式。

      【讨论】:

      • 聪明 :) 我还没想好,但听起来可行。我有兴趣看看 MS 的任何人是否就这个问题发表意见
      【解决方案6】:

      我有同样的想法并实现了一个JsonPox 过滤器来做到这一点。

      【讨论】:

      • 痘?如果你想让人们使用它,你不应该把它命名为 Pox :-)
      【解决方案7】:

      或者,如果您不想使用反射,您可以使用结果的 Data 属性创建一个 RouteValueDictionary。使用 OP 的数据...

      var jsonData = new RouteValueDictionary(result.Data);
      Assert.IsNotNull(jsonData);
      
      Assert.AreEqual(2,
                      jsonData.Keys.Count);
      
      Assert.AreEqual("123",
                      jsonData["foo"]);
      
      Assert.AreEqual(true,
                      jsonData["success"]);
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-04-29
        • 2013-03-25
        • 2013-12-26
        • 2012-01-12
        • 2010-11-26
        • 1970-01-01
        • 2011-09-08
        相关资源
        最近更新 更多