Пишем код

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

Archive for the ‘.net’ Category

Презентация и исходники с вводного семинара по NHibernate

2 комментария

В рамках MccTomskCamp 18 июля проводил небольшой вводный семинар по NHibernate. Ссылки на видео, презентацию и исходные коды можно найти в конце поста, а можно и посмотреть прямо здесь:

NHibernate Intro

More PowerPoint presentations from Alexander Sidorov

P.S. Большое спасибо всем посетившим семинар за проявленное внимание к мероприятию :)/a

Written by Shaddix

Август 1st, 2012 at 12:57 пп

Ускорение билда asp.net MVC в 10 раз или RazorGenerator как альтернатива MvcBuildViews

without comments

Долгое время одним из первых шагов после создания нового MVC-сайта у меня было редактирование .csproj и добавление магического элемента

<mvcbuildviews>true</mvcbuildviews>

Напомню, что этой директивой включается компиляция Razor-вьюшек, таким образом об ошибках в .cshtml файлах можно узнать не в рантайме (при открытии веб-странички), а во время компиляции проекта. Опция, безусловно, очень удобная, но очень сильно замедляющая процесс сборки. Так, при отключенной опции наш средних размеров MVC-проект собирается за 1 секунду, а со включенной — за 10 секунд (в проекте всего лишь чуть больше 100 cshtml файлов).
Такая немаленькая задержка очень серьезно сказывается на разработке — по сути при каждом билде можно легко успеть «переключиться» на браузер и пролистать пару страничек :)

Таким образом от MvcBuildViews очень захотелось отказаться, но при этом не потерять возможность обнаружения ошибок во вьюшках во время компиляции.
Проблему решил замечательный RazorGenerator от Дэвида Эббо. РазорГенератор интегрируется в качестве Custom Tool для Visual Studio и преобразует каждую вьюшку в соответствующий c#-код, который и компилируется в момент компиляции проекта и выдает ошибки абсолютно аналогичные MvcBuildViews. При этом время компиляции с RazorGenerator’ом составляет(в нашем случае) ту же 1 секунду, что и без него (с отключенным MvcBuildView). Итоговый выигрыш — в 10 раз (10 сек — компиляция + MvcBuildViews, 1 сек — компиляция + RazorGenerator).
Read the rest of this entry »

Written by Shaddix

Июль 15th, 2012 at 1:40 пп

ModelUnbinder в T4MVC — работа со сложными моделями

without comments

В рамках ModelUnbinder, об основном сценарии использования которого я уже писал, была решена еще одна немаловажная проблема — работа со «сложными» моделями.
Рассмотрим пример: допустим, у нас на сайте есть форма поиска, и есть экшен, который показывает результаты:

        public class SearchModel
        {
            public string SearchString { get; set; }
            public bool InLocalArea { get; set; }
            public int Page { get; set; }
            public int PerPage { get; set; }
        }

        public virtual ActionResult Search(SearchModel searchModel)
        {
            return Json(searchModel, JsonRequestBehavior.AllowGet);
        }

Допустим, мы хотим дать ссылку на результаты какого-то поиска и хотим воспользоваться для этого T4MVC. Попробуем так:

<a href="@Url.Action(MVC.Home.Search(new HomeController.SearchModel()
             {
                 SearchString = "qweasd",
             }))">Предзабитый поиск1</a>

В результате у нас получится URL вроде: http://localhost:30729/Home/Search?searchModel=MvcApplication2.Controllers.HomeController.SearchModel. Это, конечно, не совсем то, что нужно :) Для решения проблемы воспользуемся ModelUnbinder’ом:

        protected void Application_Start()
        {
            //blablabla
            ModelUnbinderHelpers.ModelUnbinders.Add(typeof(HomeController.SearchModel), new PropertiesUnbinder());
        }

PropertiesUnbinder — это анбайндер, поставляемый в комплекте с T4MVC. Собственно, с его помощью в данном случае мы и получаем требуемый результат, ссылка теперь выглядит таким образом: http://localhost:30729/Home/Search?searchModel.SearchString=qweasd&searchModel.InLocalArea=False&searchModel.Page=0&searchModel.PerPage=0. Конечно, и этот URL можно чуть-чуть улучшить (например, не писать дефолтные значения value-type’ов), но по крайней мере эта ссылка вполне корректно работает, а в исходном коде нам пришлось поменять всего одну строчку — добавление анбайндера.

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

        public interface IComplexModel { }
        public class SearchModel : IComplexModel
        {
            public string SearchString { get; set; }
            /* ... */
        }

        protected void Application_Start()
        {
            //blablabla
            ModelUnbinderHelpers.ModelUnbinders.Add(typeof(IComplexModel), new PropertiesUnbinder());
        }

Вот и всё! Можно лишь добавить, что PropertiesUnbinder отлично работает и с массивами внутри модели, то есть модель вида:

        public class SearchModel : IComplexModel
        {
            public int[] Numbers { get; set; }
        }

так же будет вполне адекватно преобразована в соответствующий URL.

Как обычно, тестовый проект с примером можно скачать и полюбопытствовать. Буду рад, если это кому-нибудь пригодиться.

Written by Shaddix

Июль 5th, 2012 at 10:56 дп

Posted in .net,agile,MVC,t4mvc,web

Model unbinder в T4MVC

without comments

В предыдущей заметке я рассказывал об удобствах использования кастомного model-binder’a для получения MVC-экшенов вида public ActionResult UserInfo(User user) (с использованием доменных классов User вместо int userId). Там же я упомянул и о неудобствах использования этого метода с T4MVC.
Последние несколько дней я активно работал над решением этой проблемы (пути решения которой мы предварительно обсудили с Дэвидом Эббо), а сегодня пулл-реквест с изменениями был принят в основную ветку (версия 2.10.0, доступна в том числе и через Nuget), и сейчас я хотел бы кратко описать, что в итоге получилось.

Для начала вспомним, в чем собственно была проблема.
Допустим, у нас есть экшен вида public ActionResult UserInfo(User user) и мы хотим сгенерировать для него URL.
Мы пишем @Url.Action(MVC.Home.UserInfo(user)) и получаем: /home/userinfo?user=MvcApplication.Models.User. Это происходит потому, что для преобразования объекта в параметр запроса MVC просто вызывает .ToString() от объекта. Нам же надо получить что-то вроде /home/userinfo?user=2 (подразумевая, что user.Id == 2).
Можно, конечно, переопределить метод .ToString() у класса User, но очень часто это решение не подходит (например, .ToString() уже переопределен человекочитаемым вариантом).

Для решения этой проблемы и получилась у нас система, обратная ModelBinder’ам, которая с легкой руки Дэвида получила название ModelUnbinder.
Суть ModelUnbinder’a противоположна ModelBinder’у, если байндер создает и инициализирует объект по значениям HttpRequest’a, то анбайндер, наоборот, формирует параметры запроса по объекту.

Анбайндер, по аналоги с байндером, должен релизовывать интерфейс IModelUnbinder или IModelUnbinder<T>.
Вот как может выглядеть, например, простейший анбайндер:

    public class NHibernateModelUnbinder : IModelUnbinder<BaseEntity>
    {
        public void UnbindModel(RouteValueDictionary routeValueDictionary, string routeName, BaseEntity routeValue)
        {
            if (routeValue != null)
                routeValueDictionary.Add(routeName, routeValue.Id);
        }
    }

Как видим, для всех объектов типа BaseEntity (предположим, что это базовый класс для наших доменных сущностей), он сохраняет в запрос идентификатор этих объектов. Таким образом при использовании @Url.Action(MVC.Home.UserInfo(user)) мы получим строку запроса вида /home/userinfo?user=2 (без использования анбайндера было бы что-то вроде /home/userinfo?user=MvcApplication.Models.User)
Осталось всего-лишь зарегистрировать анбайндер в T4MVC. Для этого где-нибудь в Application_Start необходимо написать что-нибудь вроде:

        protected void Application_Start()
        {
            //...some initialization code

            ModelBinders.Binders.Add(typeof(MyUser), new MyUserBinder());  // регистрируем кастомный MVC-шный байндер 
            ModelUnbinderHelpers.ModelUnbinders.Add(new MyUserUnbinder()); // регистрируем T4MVC-анбайндер
        }

Вот и всё! Можно скачать простейший тестовый пример с применением анбайндеров в T4MVC (и байндеров — в ASP.NET MVC :)), а можно и почитать подробнее про использование кастомных байндеров для удобства обращения с БД-сущностями в asp.net mvc (там же можно посмотреть и пример байндера для NHibernate-сущностей)

Written by Shaddix

Июль 3rd, 2012 at 10:23 пп

Posted in .net,agile,MVC,t4mvc

Локализация сообщений о переполнении int в MVC3

without comments

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

Однако недавно в проекте я столкнулся с одной ошибкой валидации, перевести которую оказалось не так просто. Вот пример этой ошибки:

Как нетрудно заметить, я просто ввел в числовое поле очень большое значение. При этом сам факт возникновения ошибки меня вполне устраивал (большие значение обрабатывать и не планировалось), а вот текст на «иностранном» тестеров слегка смутил :)

Read the rest of this entry »

Written by Shaddix

Июнь 23rd, 2012 at 6:31 пп

Posted in .net,MVC

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

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

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

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

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

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

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

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

Written by Shaddix

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

Posted in .net,agile,MVC,web

Обновление БД с nHibernate/FluentMigrator и проблемы с индексами/дефолтными констрейнтами

without comments

Вопрос миграций — обновления БД со старых на новые версии для синхронизации с текущим состоянием — неминуемо встает перед разработчиками, как только их приложения уходят в «продакшн». До первого релиза во многих случаях вся команда разработчиков работает с одной базой, так что этот вопрос может не стоять так остро. Но как только приложение появляются первые продакшн/staging/testing сервера, ситуация, когда разработчики понаписали код и пора бы обновить релизную БД неминуемо возникает :)

Если релизы происходят нечасто, то для ручного решения таких проблем хорошим решением может стать SqlCompare от RedGate, которая эту задачу предельно упрощает. Но если у вас коробочный продукт, или вы хотите обновлять staging сервер по билду CI-сервера, то, конечно, рано или поздно захочется этот процесс автоматизировать.
Read the rest of this entry »

Written by Shaddix

Май 28th, 2012 at 11:00 дп

Posted in .net,agile,nHibernate

Юнит-тестирование БД-зависимых классов

without comments

Написание автоматизированных тестов для приложений работающих с БД часто становится источником многочисленных споров. Основная причина этих споров — подходы к тестированию. Можно выделить 3 основных подхода, которые при этом применяются:

  1. Абстрагирование от БД. Подход предполагает, что взаимодействия с какими бы то ни было БД в тестах не происходит. Для этого все обращения к БД оборачивают в вызовы, например, IRepository и в тестах эти обращения замещаются с помощью mock-фреймворков (Rhino.Mocks, Moq, etc.). Из плюсов подхода можно выделить популярность паттерна репозиторий, что ускоряет знакомство с методикой. Из минусов — приходится замещать все БД-обращения (иногда их бывает довольно много), а также тесты жестко связываются с интерфесом репозиториев (в случае изменения интерфесов тесты придется также изменять)
  2. Работа поверх тестовой БД. В тестах используется база, «похожая» на продакшн-базу, но с некоторыми тестовыми данными. Тесты, таким образом, работают в окружении, максимально близком к реальной среде, что, без сомнения, является большим плюсом. Из минусов можно выделить более низкую скорость выполнения тестов и проблемы поддержания тестовой БД в эталонном состоянии.
  3. Работа поверх временных БД. В тестах используются временные базы, чаще всего, способные хранить структуру в памяти (SQLite) или на локальном диске (SQL CE). Из плюсов — высокая скорость работы и «приближенность» к реальному окружению (хоть и менее близкая, чем во втором пункте).

Стоит отметить и общую часть всех трех подходов в «простых» случаях: участки кода, которые не требуют «активного» доступа к БД, а лишь модифицируют сущности доменной модели, тестируются во всех подходах одинаково и взаимодействия с БД просто не требуют.

В своей работе я не использую репозитории, потому что в большинстве случаев они являются ненужной прослойкой, которая лишь усложняет доступ к данным.
Из других подходов, я стараюсь использовать последний, поскольку он наиболее лёгок и гибок в применении. Он особенно удобен при использовании ORM, поскольку в этом случае сам ORM обычно способен создать схему базы в соответствии с определенными маппингами и соглашениями.
Ниже я приведу пример базового тестового класса, при наследовании от которого я получаю временную sqlite базу со структурой, соответствующей текущему проекту, и 3 NHibernate-сессии открытые поверх этой БД. Несколько сессий я использую для удобства проверки коммитов: обычно настройку начальных данных я провожу в sessionArrange, в тестируемый код передается sessionAct и проверки осуществляются через sessionAssert, таким образом все «несохраненные» в тестируемом коде данные из sessionAssert будут просто не видны.

    public class InMemoryDb : IDisposable
    {
        private static Configuration Configuration;
        private static ISessionFactory SessionFactory;
        protected ISession _sessionAssert;
        protected ISession _sessionArrange;
        protected ISession _sessionAct;


        [SetUp]
        public void _Setup()
        {
            Init();
        }

        public void Init()
        {
            if (Configuration == null)
            {
                var conf = Fluently.Configure()
                    .Database(SQLiteConfiguration.Standard.InMemory)
                    .Mappings(m => m.AutoMappings.Add(AutomappingGenerator.CreateAutomappings)); 
                     //AutomappingGenerator.CreateAutomappings - статичная функция, возвращающая AutoPersistenceModel - конфигурацию NHibernate в текущем проекте.

                SessionFactory = conf.BuildSessionFactory();
                Configuration = conf.BuildConfiguration();
            }

            _sessionAct = SessionFactory.OpenSession();
            _sessionArrange = SessionFactory.OpenSession(_sessionAct.Connection);
            _sessionAssert = SessionFactory.OpenSession(_sessionAct.Connection);

            _sessionAct.BeginTransaction();
            _sessionArrange.BeginTransaction();
            _sessionAssert.BeginTransaction();

            var nullWriter = new StringWriter();
            new SchemaExport(Configuration).Execute(false, true, false, _sessionAct.Connection, nullWriter);
        }


        public void Dispose()
        {
            _sessionAct.Dispose();
            _sessionArrange.Dispose();
            _sessionAssert.Dispose();
        }
    }

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

Written by Shaddix

Май 2nd, 2012 at 11:08 дп

Проблема one-click publish и MvcBuildViews в asp.net MVC

without comments

При использовании one-click publish я как-то раз столкнулся с подобной ошибкой:

It is an error to use section registered as allowDefinition=’MachineToApplication’ beyond application level. This error can be caused by a virtual directory not being configured as an application in IIS.

Происходит она при попытке компиляции проекта после первого же паблиша (на удаленный сервер или даже в локальную папку).
Столкнувшись сегодня с ней во второй раз и осознав, что я не помню, «что это было» и как это лечится, решил написать заметку.
Read the rest of this entry »

Written by Shaddix

Апрель 17th, 2012 at 6:59 пп

Хитрость байндинга булевых полей в asp.net mvc

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

Сегодня мне вдруг стало любопытно, как происходит байндинг булевых полей в MVC.
Причину любопытства можно пояснить на примере:
Допустим, у нас есть модель:

public class TestModel {
   public string Name {get;set;}
   public bool IsDeleted {get;set;}
}

И есть две хтмл-формы для редактирования этой модели, в первой из которых присутствует флаг IsDeleted, а во второй — нет:
Форма 1:

@using (Html.BeginForm) {
    @Html.TextboxFor(x => x.Name)
    @Html.CheckboxFor(x => x.IsDeleted)
    <input type="submit" value="OK" />
}

Форма 2:

@using (Html.BeginForm) {
    @Html.TextboxFor(x => x.Name)
    <input type="submit" value="OK" />
}

При этом серверный обработчик этой формы выглядит вполне типично:

public ActionResult UpdateModel(int id) {
  var model = Db.Load<TestModel>(id);
  TryUpdateModel(model);
  return RedirectToAction();
}

Предположим, что в базе у TestModel IsDeleted == true. Что произойдет, если

  • мы отсабмитим Форму1 со снятым флагом?
  • мы отсабмитим Форму2, в которой флага IsDeleted просто нет?

Как и ожидалось, в обоих случаях MVC отработал отлично, и в случае 1 в базе у сущности TestModel поле IsDeleted стало false, а в случае 2 — осталась в true.
В чем же причина любопытства? Мне было любопытно, как же именно это работает.
В «классическом html» чекбоксы обычно представлены в виде <input name=’IsDeleted’ type=’checkbox’ />. А при сабмите формы на сервер это отправляется в виде:

  • http://localhost/?name=zcx&IsDeleted=on — если флажок проставлен
  • http://localhost/?name=zcx — если флажок не стоит

Как видно, в «классическом случае» ситуация отсутствия чекбокса в форме как такового и ситуация, когда он есть, но «галочка не стоит» абсолютно одинаковы, и выполнить корректный байндинг не представляется возможным.

Именно поэтому мне было интересно заглянуть «под капот» и узнать, как же это реализовано в mvc3.
Решение оказалось простым: на каждое булево поле mvc генерит такой html:

<input type="checkbox" name="IsDeleted" value="true" /><input type="hidden" name="IsDeleted" value="false" />

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

Written by Shaddix

Апрель 15th, 2012 at 10:52 пп

Posted in .net,MVC,web