Техники связывания введенных данных с Моделью в ASP.NET MVC

Предположим, в веб-приложении есть некий класс. При этом необходимо чтобы данные с формы заполняли только часть его свойств. Как поступить в этом случае? Давайте рассмотрим несколько подходов для решения этой проблемы.

Исходная Модель

Перед тем как начать, посмотрим на Модель для примеров. Пусть это будет статья в блоге:

public class Article
{
    public string Title { get; set; }
    public string Content { get; set; }   
    public bool IsPublished { get; set; }
}

По логике приложения решение о публикации принимает только модератор, а не сами авторы. Таким образом, требуется при передаче ими данных на сервер не заполнять свойство isPublished. Разумеется, необходимо учесть, что недобросовестный пользователь может подставить значение в запрос. Поэтому только на отсутствие соответствующего поля ввода полагаться нельзя.

Техника с неявной связью

Данная техника использует атрибут ASP.NET MVC Bind для параметра Действия. Он позволяет определить, какие свойства должны быть заполнены или проигнорированы. Например, для описанного выше сценария это будет выглядеть так:

[HttpPost]
public ViewResult Edit([Bind(Include = "Title,Content")] Article article)
{
    ...
}

Здесь Bind указывает заполнять только два свойства: Title и Content. Но, в зависимости от задачи, можно перечислить игнорируемые свойства:

[HttpPost]
public ViewResult Edit([Bind(Exclude = "IsPublished")] Article article)
{
    ...
}

В любом из вариантов, даже если злоумышленник вручную поставит в запрос значение IsPublished, оно не будет передано в экземпляр Модели.

И еще один вариант, который эквивалентен использованию Bind с указанием Include:

[HttpPost]
public ViewResult Edit()
{
    var article = new Article();
    this.TryUpdateModel(article, includeProperties: new[] { "Title", "Content" });

    ...
}

Необходимо отметить явный минус этого подхода: имена свойств указаны в текстовом виде. А значит, что никаких ошибок на этапе компиляции, при их неправильном написании, не будет. Автоматически отследить подобные ситуации можно только с помощью модульного тестирования (unit testing).

Техника со сильным связыванием

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

public interface IEditableArticle
{
    string Title { get; set; }
    string Content { get; set; }
}

Разумеется, Модель также должна его реализовывать:

public class Article : IEditableArticle
{
    public string Title { get; set; }
    public string Content { get; set; }   
    public bool IsPublished { get; set; }
}

Тогда Действие будет выглядеть следующим образом:

[HttpPost]
public ActionResult Edit()
{
    var article = new Article();
    this.TryUpdateModel<IEditableArticle>(article);
    ...
}

Недостатком данной техники является необходимость наследования Моделью интерфейса, что не всегда желательно и возможно.

Архитектурный подход

Средние и большие решения, как правило, используют многослойную архитектуру. В такой ситуации, все ASP.NET MVC приложение может рассматриваться как один из элементов слоя UI. При этом, наверное самым удачным вариантом является создание своей внутренней модели для пользовательского ввода:

public class ArticleViewModel
{
    public string Title { get; set; }
    public string Content { get; set; }
}

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

public static class ArticleViewModelExtensions
{
    public static Article ToServiceTypeArticle(this ArticleViewModel articleViewModel)
    {
        return new Article() {
            Title = articleViewModel.Title,
            Content = articleViewModel.Content,
            IsPublished = false
        };
    }
}

Соответственно, Действие примет вид:

[HttpPost]
public ViewResult Edit(ArticleViewModel article)
{
    ...
    this.SomeArticleService.Update(article.ToServiceTypeArticle());
    ...
}

Из сложностей архитектурного подхода можно отметить необходимость согласования атрибутов основной Модели и Модели для пользовательского ввода.

Запрет на изменение свойства

Необходимо упомянуть еще об одном способе. Он доступен с 3 версии ASP.NET MVC и подходит, если значение свойства никогда не должно быть заполнено данными из Представления. В этом случае достаточно отметить его атрибутом ReadOnly:

public class Article
{
    public string Title { get; set; }
    public string Content { get; set; }
    
    [ReadOnly(true)] 
    public bool IsPublished { get; set; }
}

В завершение стоит напомнить одну прописную истину: никогда не доверяйте вводимым данным.

Комментарии (8) -

Артём 29.06.2013 17:33:04

Помойму самый адекватный способ это "Архитектурный подход" + AutoMapper и не каких проблем нет.

@ Артём: Для просто моделей да. Но все же остаются атрибуты. Хотя их можно автоматически согласовывать с помощью своего провайдера атрибутов.

Артём 29.06.2013 18:00:26

Andrey :
@ Артём: Для просто моделей да. Но все же остаются атрибуты. Хотя их можно автоматически согласовывать с помощью своего провайдера атрибутов.

Ну да есть такое. Правда я не пользуюсь чистым Asp.Net MVC, использую https://github.com/MvcExtensions/Core. Там вроблему с атрибутами решается легче т.к. их там нету.

@ Артём: Мне кажется мы о разном. Я про атрибуты Модели. Если в приложении несколько слоев, то модели могут быть определены в BL. Там, на свойствах могут быть установлены ограничения по длине строки [StringLength(n)], обязательного заполнения [Required] и т.д. Если такую Модель сразу передать во Представление, то будет срабатывать валидация на стороне клиента.

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

Артём 29.06.2013 18:34:54

@Andrey Ну да) а разном, я почему то подумал сразу о том, что у нас 2 модели ModelView могут работать с одной моделью BL. И можно случайно сделать разные параметры валидации для этих одних и тех же полей, для этих двух ModelView. (Это я про клиентскую валидацию)

Отличная статья!!! О некоторых методиках не знал. Спасибо автору.

Меня еще волнует наилучший способ восстановления модели после выполнения POST запроса. Писать подобный код в GET и POST - вообще не радует. Пытался выносить в отдельный метод, но тоже не лучшее решение. Вообщем думаю еще...

@ Дмитрий: А что под восстановлением понимаете?

Под "восстановлением", я думаю, @ Дмитрий имеет в виду заполнение SelectList*ов и прочего))) Я конечно могу ошибаться. Но если это так - то на этот вопрос я могу дать ответ. Создается паблик метод в модели для заполнения листов. И просто выполняется пере возвратом модели.    

Добавить комментарий