Должен ли мой контроллер MVC действительно знать о JSON?

Класс JsonResult - очень полезный способ вернуть Json в качестве действия для клиента через AJAX.

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

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

Однако (по крайней мере, по моему первому впечатлению) это действительно не хорошее разделение проблем.

  • Unit test методы сложнее писать, потому что у них нет хороших строго типизированных данных для тестирования и они должны знать, как интерпретировать Json.
  • Это сложнее для некоторых других представлений в будущем, которые не связаны с HTTP (или любым удаленным протоколом, включающим сериализацию), чтобы быть "подключенным", потому что его ненужным в таких случаях является сериализация и десериализация ответа.
  • Что делать, если у вас есть два разных места, которые нуждаются в результатах этого действия? Один хочет Json, а другой хочет XML или, возможно, полностью или частично визуализированное представление.

Мне интересно, почему трансляция между объектом и 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)
       };
}

Мне было просто любопытно относиться к мыслям людей и к любой альтернативной схеме.

Это также мои первоначальные наблюдения - вероятно, есть еще больше причин, почему это не идеальный дизайн (и, вероятно, много причин, почему его вполне приемлемый и практичный!) Я просто чувствую теоретическую и бесовскую защиту сегодня вечером.

  * Отказ от ответственности: я даже не задумывался о том, как будет реализован атрибут или какие побочные эффекты или рекурсии и т.д. он может иметь.

Ответ 1

Я думаю, что ты не справляешься ни с чем. Итак, что, если контроллер знает о JSON в своем публичном интерфейсе?

Мне однажды сказали: "Сделайте свой код общим, не делайте свое приложение общим".

Здесь вы пишете Application Controller. Это нормально для Контроллера приложений, чья ответственность заключается в том, чтобы уменьшить модель и представления и вызвать изменения в модели - знать о определенном виде (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), принимают аргумент "format":

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-вызовов, независимо от того, что вы хотите назвать их, может выводиться в различных форматах.

Ответ 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, чтобы не нарушать ваши тесты.

Ответ 3

Я не слишком беспокоюсь о возвращении JSon, как и раньше. Характер AJAX, по-видимому, таков, что сообщение, которое вы хотите обработать в Javascript, применяется только для ситуации AJAX. Необходимость AJAX для производительности просто должна каким-то образом повлиять на код. Вероятно, вы не захотите возвращать те же данные другому клиенту.

Пара вопросов, связанных с тестированием JSonResult, который я заметил (и мне еще нужно написать какие-либо тесты для моего приложения):

1), когда вы возвращаете JSonResult из вашего метода действий, который "получен" вашим тестовым методом, вы все равно имеете доступ к исходному объекту 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;
}

Unit test:

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

Я думаю, что у вас есть правильная точка - почему бы не делегировать "принятые типы ответов против генерируемого разрешения типов ответов" в какое-то место, где оно действительно принадлежит?

Это напоминает мне одно из мнений Джереми Миллера о том, как сделать приложение ASP.NET MVC: "Мнения" на ASP.NET MVC

В своем приложении все действия контроллера имеют скудный и простой интерфейс - в него входит некоторый объект модели представления, другой объект модели представления уходит.

Ответ 5

Я не уверен, насколько большая проблема на самом деле, но "альтернативный шаблон" для ASP.NET MVC должен был бы написать JSON ViewEngine. На самом деле это было бы не так сложно, так как функциональность JSON, встроенная в структуру, сделает для вас большую часть тяжелой работы.

Я думаю, что это будет лучший дизайн, но я не уверен, что это намного лучше, чем это стоит против "официального" способа внедрения JSON.

Ответ 6

У меня была такая же мысль и был реализован фильтр JsonPox, чтобы сделать это.

Ответ 7

В качестве альтернативы, если вы не хотите использовать отражение, вы можете создать RouteValueDictionary с результатом свойства Data. Переход с данными 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"]);