Должен ли мой контроллер MVC действительно знать о JSON? - PullRequest
22 голосов
/ 27 января 2009

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

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.

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

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

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

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

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

Ответы [ 7 ]

10 голосов
/ 27 января 2009

Я думаю, ты переживаешь из-за ничего. Так что, если контроллер знает о 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 s даже достаточно умен для вывода таблиц, если их объект реализует 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, как бы вы их ни называли, могут выводиться в различных форматах.

7 голосов
/ 27 января 2009

Я вообще стараюсь не беспокоиться об этом. В 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, чтобы не прерывать ваши тесты.

4 голосов
/ 03 февраля 2009

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

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

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

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

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

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

1 голос
/ 30 марта 2009

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

1 голос
/ 27 января 2009

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

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

0 голосов
/ 25 марта 2009

Кроме того, если вы не хотите использовать рефлексию, вы можете создать RouteValueDictionary со свойством Data результата. Идем с данными ОП ...

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"]);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...