关于asp.net mvc:我的MVC控制器真的应该知道JSON吗?

Should my MVC controller really know about JSON?

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

1
2
3
4
5
6
7
8
9
public JsonResult JoinMailingList(string txtEmail)
{
        // ...

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

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

  • 单元测试方法更难写,因为它们没有很好的强类型数据来测试,必须知道如何解释Json。
  • 未来的其他一些View更难以通过HTTP(或任何涉及序列化的远程协议)被"插入",因为在这种情况下,它不需要序列化和反序列化响应。
  • 如果您有两个不同的地方需要该行动的结果怎么办?一个人想要Json,另一个人想要XML或者完全或部分渲染的视图。

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

单元测试可以采取动作结果(ObjectActionResult)并拉出强类型Foo

1
2
3
4
5
6
7
8
9
10
[JsonConvertible]
public ActionResult JoinMailingList(string txtEmail)
{
        // ...

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

我只是好奇人们的想法和任何可供选择的模式。

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

*免责声明:我甚至没有开始考虑如何实施属性或者它可能具有哪些副作用或重复性等。


我觉得你无所事事。那么如果控制器在其公共接口中知道JSON呢?

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

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

在我自己的项目中,我通常有类似的东西:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface IFormatter {
    ActionResult Format(object o);
}
class HtmlFormatter : IFormatter {
    // ...
}
class JsonFormatter : IFormatter {
    // ...
}
class PlistFormatter : IFormatter {
    // ...
}
class XmlFormatter : IFormatter {
    // ...
}

基本上是"格式化程序",它接受对象并为它们提供不同的表示。 HtmlFormatter甚至足够聪明,如果它们的对象实现IEnumerable,则输出表。

现在,返回数据的控制器(或者可以使用HtmlFormatter s生成网站部分的控制器)采用"格式"参数:

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

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

1
2
3
4
5
6
7
class ObjectFormatter : IFormatter {
    ActionResult Format(object o) {
        return new ObjectActionResult() {
            Data = o
        };
    }
}

使用此方法,您的任何查询/操作/过程/ ajax调用,无论您想要调用它们,都可以以各种格式输出。


我一般不要担心它。 Asp.Net MVC足以解决问题,将泄漏降至最低。你是对的;测试时有一点障碍。

这是我使用的测试助手,它运行良好:

1
2
3
4
5
6
7
8
9
10
11
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类型:

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

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


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

关于我已经注意到的JSonResult测试的几点事情(我还没有为我的应用程序编写任何测试):

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

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

首先使用样品:

行动方式:

1
2
3
4
5
6
7
8
9
10
11
12
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult ContactUsForm(FormCollection formData){

     // process formData ...

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

     return result;
}

单元测试:

1
2
3
4
5
6
7
8
9
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
}

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

扩展方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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;
}

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

它让我想起了Jeremy Miller关于如何制作ASP.NET MVC应用程序的一个观点:ASP.NET MVC上的"意见"

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


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


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

我确实认为这将是一个更好的设计,但我不确定它是否更好,它值得违背实现JSON的"官方"方式。


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

1
2
3
4
5
6
7
8
9
10
11
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"]);