我的MVC控制器真的应该知道JSON吗?

时间:2009-01-27 05:07:43

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

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()转换的属性。

单元测试只需获取操作结果(ObjectActionResult)并提取强类型Foo

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

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

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

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

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

7 个答案:

答案 0 :(得分:10)

我认为你正在努力工作。那么如果控制器在其公共接口中知道JSON会怎样呢?

我曾被告知:“让你的代码变得通用,不要让你的应用程序变得通用。”

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

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

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

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

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

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调用,无论您想要调用它们,都可以以各种格式输出。

答案 1 :(得分:6)

我一般不要担心。 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来不破坏你的测试。

答案 2 :(得分:4)

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

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

1)当您从您的测试方法“接收”的操作方法返回JSonResult时,您仍然可以访问原始Data对象。起初这对我来说并不明显(尽管有些明显)。 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;
}

答案 3 :(得分:2)

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

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

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

答案 4 :(得分:1)

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

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

答案 5 :(得分:1)

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

答案 6 :(得分:0)

或者,如果您不想使用反射,可以使用结果的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"]);