Пишем код

Заметки о .net разработке

Используем инфраструктуру MVC3 — скажи НЕТ повторяющимся session.Load(id);

6 комментариев

Раньше в веб-проектах на asp.net mvc у меня часто встречался повторяющийся код типа:

<br />
public ActionResult UserProfile(int id) {<br />
  var user = Session.Load<User>(id);<br />
  // do something...<br />
}<br />

Действительно, этот код очень типичен, обычно все объекты адресуются по ID и экшены обычно оперируют с этими объектами. Поэтому аргументами экшенов часто становятся идентификаторы, и где-то в начале экшенов мы запрашиваем из БД собственно объекты.

Задуматься над этим кодом меня заставил недавний пост Айенды. Немного подумав, стало ясно, что подобные участки очень просто автоматизировать и превратить во что-то вроде:

<br />
public ActionResult UserProfile(User user) {<br />
  // do something...<br />
}<br />

Дополнительным плюсом такой конструкции будет большая читаемость (очевидно, что метод работает именно с пользователем, а не с абстрактным int id) и строго-типизированность при вызове из Url.Action, RenderAction и тестов.

С технической стороны реализация также не представляется сложной: asp.net mvc прекрасно расширяется своими ModelBinder’ами, при этом URL запроса к этому экшену не изменится, оставшись похожим на нечто вроде: /Home/UserProfile?user=12. Как видим, мы по прежнему передаем ID в запросах, а в ModelBinder’е будем лишь загружать из базы требуемые сущности.
Упрощенный пример реализации ModelBinder’a:
<br />
public abstract class DomainEntitiesModelBinder : DefaultModelBinder<br />
    {<br />
        protected internal abstract object GetObject(Type type, object id);</p>
<p>        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)<br />
        {<br />
            var modelName = bindingContext.ModelName;</p>
<p>            if (string.IsNullOrEmpty(modelName))<br />
            {<br />
                //this part will be run on TryUpdateModel()<br />
                return base.BindModel(controllerContext, bindingContext);<br />
            }<br />
            else<br />
            {<br />
                var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);<br />
                var valueType = bindingContext.ModelType;</p>
<p>                if (valueResult != null)<br />
                {<br />
                    var id = valueResult.ConvertTo(typeof(int)) as int?;</p>
<p>                    if (id != null && id != 0)<br />
                    {<br />
                        return GetObject(valueType, id);<br />
                    }<br />
                }<br />
                return null;<br />
            }<br />
        }<br />
    }<br />

Пример упрощен для простоты понимания, полный текст базового байндера — в конце поста.
Для его использования необходимо отнаследоваться от приведенного базового байндера, например, таким образом (в случае NHibernate):
<br />
public class DomainEntitiesNHModelBinder : DomainEntitiesModelBinder<br />
    {<br />
        protected override object GetObject(Type type, object id)<br />
        {<br />
            return DependencyResolver.Current.GetService<ISession>().Load(type, id);<br />
        }<br />
    }<br />

таким образом id типа int будет прочитан из запроса базовым байндером и передан в функцию GetObject наследника, который и загрузит сущность из БД.

Регистрация байндера в MVC будет выглядеть так (где-нибудь в Application_Start Global.asax’a:

<br />
     ModelBinderProviders.BinderProviders.Add(<br />
            new InheritanceAwareModelBinderProvider<br />
                {<br />
                    { typeof (BaseEntity),  new DomainEntitiesNHModelBinder() }<br />
                });<br />

И, наконец, код небольшого класса, который позволяет назначить байндер всем классам-наследникам базового (в моей практике доменные классы часто наследуются от базового — BaseEntity в примере выше)
<br />
    /// </p>
<summary>
    /// Adds inheritance support when registering model binders.<br />
    /// Any model binders added here will be invoked if the Type being bound inherits from the type registered.<br />
    /// </summary>
<p>    public class InheritanceAwareModelBinderProvider : Dictionary<Type, IModelBinder>, IModelBinderProvider<br />
    {<br />
        public IModelBinder GetBinder(Type modelType)<br />
        {<br />
            var binders = from binder in this<br />
                          where binder.Key.IsAssignableFrom(modelType)<br />
                          select binder.Value;</p>
<p>            return binders.FirstOrDefault();<br />
        }<br />
    }<br />

Использование подобного байндера особенно удобно в паре с опцией batch-size НХибернейта, поскольку в простых случаях позволяет получать значения связанных таблиц путем ленивой загрузки.
Конечно, данный способ может иметь и негативное влияние на производительность, если, в частности, запросы на сумму-количество и прочие аггрегатные функции будут производиться через linq по данному объекту, а не запросом к БД. Будьте осторожны и не забывайте думать при использовании любых решений :)

P.S. А вот и полный текст реализации реального model-binder’a. Он достаточно сложен и разрастался по мере столкновения с различными ситуациями, связанными с его использованием (использование из UrlAction/RenderAction/обычных запросов/TryUpdateModel/etc):

<br />
public abstract class DomainEntitiesModelBinder : DefaultModelBinder<br />
    {<br />
        protected internal abstract object GetObject(Type type, object id);</p>
<p>        [ThreadStatic]<br />
        private static bool UseDefaultBinder;<br />
        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)<br />
        {<br />
            var modelName = bindingContext.ModelName;</p>
<p>            if (string.IsNullOrEmpty(modelName))<br />
            {<br />
                //this part will be run on TryUpdateModel()<br />
                try<br />
                {<br />
                    UseDefaultBinder = true;<br />
                    var defaultResult = base.BindModel(controllerContext, bindingContext);<br />
                    return defaultResult;<br />
                }<br />
                finally<br />
                {<br />
                    UseDefaultBinder = false;<br />
                }<br />
            }<br />
            else<br />
            {<br />
                var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);<br />
                var valueType = bindingContext.ModelType;</p>
<p>                if (valueResult != null)<br />
                {<br />
                    if (valueType.IsInstanceOfType(valueResult.RawValue))<br />
                    {<br />
                        return valueResult.RawValue;<br />
                    }<br />
                    var id = valueResult.ConvertTo(typeof(int)) as int?;</p>
<p>                    if (id != null && id != 0)<br />
                    {<br />
                        var result = GetObject(valueType, id);</p>
<p>                        if (UseDefaultBinder)<br />
                        {<br />
                            ModelBindingContext newBindingContext = CreateComplexElementalModelBindingContext(controllerContext, bindingContext, result);</p>
<p>                            // validation<br />
                            if (OnModelUpdating(controllerContext, newBindingContext))<br />
                            {<br />
                                BindProperties(controllerContext, newBindingContext);<br />
                                OnModelUpdated(controllerContext, newBindingContext);<br />
                            }<br />
                        }<br />
                        return result;<br />
                    }<br />
                }<br />
                else if (UseDefaultBinder)<br />
                {<br />
                    return base.BindModel(controllerContext, bindingContext);<br />
                }<br />
                return null;<br />
            }<br />
        }</p>
<p>        internal ModelBindingContext CreateComplexElementalModelBindingContext(ControllerContext controllerContext, ModelBindingContext bindingContext, object model)<br />
        {<br />
            BindAttribute bindAttr = (BindAttribute)GetTypeDescriptor(controllerContext, bindingContext).GetAttributes()[typeof(BindAttribute)];<br />
            Predicate<string> newPropertyFilter = (bindAttr != null)<br />
                ? propertyName => bindAttr.IsPropertyAllowed(propertyName) && bindingContext.PropertyFilter(propertyName)<br />
                : bindingContext.PropertyFilter;</p>
<p>            ModelBindingContext newBindingContext = new ModelBindingContext()<br />
            {<br />
                ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, bindingContext.ModelType),<br />
                ModelName = bindingContext.ModelName,<br />
                ModelState = bindingContext.ModelState,<br />
                PropertyFilter = newPropertyFilter,<br />
                ValueProvider = bindingContext.ValueProvider<br />
            };</p>
<p>            return newBindingContext;<br />
        }<br />
        private void BindProperties(ControllerContext controllerContext, ModelBindingContext bindingContext)<br />
        {<br />
            IEnumerable<PropertyDescriptor> properties = GetFilteredModelProperties(controllerContext, bindingContext);<br />
            foreach (PropertyDescriptor property in properties)<br />
            {<br />
                BindProperty(controllerContext, bindingContext, property);<br />
            }<br />
        }<br />
    }<br />

P.P.S. К сожалению, использование данного подхода в паре с T4MVC ведет к некоторым проблемам. Мы попробуем их решить в ближайшее время в рамках T4MVC

Опубликовать в Facebook
Опубликовать в Google Plus

Written by Shaddix

Июнь 9th, 2012 at 5:51 пп

Posted in .net,agile,MVC,web

6 комментариев to 'Используем инфраструктуру MVC3 — скажи НЕТ повторяющимся session.Load(id);'

Subscribe to comments with RSS or TrackBack to 'Используем инфраструктуру MVC3 — скажи НЕТ повторяющимся session.Load(id);'.

  1. Молодец, что написал Дэвиду об этом. И, наверное, молодец, что написал не на SO. SO не располагает к подобным дискуссиям (что и подтвердил мой вопрос на аналогичную тему).

    Ты так и не написал Дэвиду по поводу интеграции твоей дваваскриптовской фигни в T4MVC?

    Пост интересный, я бы на твоем месте перепостил его на Хабр.

    AlexIdsa

    10 Июн 12 at 16:35

  2. разбодаюсь с ModelUnbinder в t4mvc, поговорим о яваскрипте :)
    для хабра слишком специфично, имхо.. на gotdotnet наверн стоит, спасибо :)

    Shaddix

    10 Июн 12 at 16:46

  3. А что делать, если нужно задать ссылку на такой action через T4MVC (предположим, что фундаментальное ограничение modeldebinding’а я обошел)? То есть я просто хочу сгенерить некоторый ActionResult, и для этого мне достаточно id, но в случае же применения вышеописанного способа мне придется считать весь объект.

    AlexIdsa

    16 Июн 12 at 03:02

  4. Собственно, одна из целей этого подхода — чтобы необходимость оперировать айдишниками не возникала, чтобы все операции проходили с доменными объектами.
    То есть по идее — ситуации «есть id, а объекта нет» возникать не должно.
    Если же она всё же возникла, то при работе с NH я просто сделаю Session.Load(id) — при этом фактического считывания объекта из БД не произойдет.

    При работе с нереляционными БД, где ситуация «есть id, объекта нет» — типична, использование данного подхода, по большому счету, лишено смысла, так как изначальная идея (все операции — только с доменными объектами) противоречит концепции nosql.
    Для устранения дублирования использовать, конечно, можно, но придется или допиливать Т4МВЦ, чтобы он генерил соответствующие методы, принимающие айдишник, а не доменный объект, или использовать что-то наподобие из моего соседнего поста.

    Shaddix

    16 Июн 12 at 14:33

  5. >>Собственно, одна из целей этого подхода — чтобы необходимость оперировать айдишниками не возникала, чтобы все операции проходили с доменными объектами.

    Разве? А по-моему цель этого подхода — иметь возможность оперировать айдишником или объектом по необходимости. Добавил этот момент в вашу дискуссию с Дэвидом.

    AlexIdsa

    16 Июн 12 at 18:44

  6. Цель этого подхода — оперировать доменными объектами :)

    Суть изменений, которые я планирую внести в Т4Мвц — возможность использования ModelUnbinder’ов, по аналогии с ModelBinder’ами.
    ModelBinder преобразует значение из запроса в объект.
    ModelUnbinder будет преобразовывать объект в значение запроса.
    Трюк с айдишниками вместо доменных объектов частью ModelUnbinder’a явно не является :)

    Shaddix

    16 Июн 12 at 22:45

Leave a Reply