Компоненты без которых не обходится ни один WEB-Проект
Рубрика: ASP .NET MVC -> Программирование -> Журнал Хакер -> Статьи
Метки: framework | mvc | SimpleMembership | программирование | советы
Просмотров: 10207
О том, что на ASP NET MVC легко и удобно создавать web-проекты ты узнал из предыдущей статьи. Приведенный пример наглядно продемонстрировал крутость фреймворка и в пух и прах разбил стереотип о неспособности Microsoft делать прикольные штуки. Теперь самое время занырнуть поглубже в бездну ASP .NET MVC и узнать, что же еще тебе готов предложить этот продукт.
Без плюшек никуда
ASP .NET MVC изначально проектировался с прицелом на масштабируемость. Разработчики не ставили перед собой задачу создать не поворотливого программного монстра на все случаи жизни. Они сделали ставку на расширяемость, и она сыграла на все сто.
На данный момент, готовых расширений для MVC фреймворка создано большое количество. К услугам искушенных разработчиков: различные шаблонизаторы, библиотеки для взаимодействия с сетью и куча всячины на все случаи жизни. Подключить один из таких пакетов дело одной команды в NuGet Console. О наиболее полезных примочках и техниках взаимодействия с ними я расскажу в этой статье.
В стиле ниндзя
Мы знаем, что идеальных технических заданий не бывает. Заказчики постоянно меняют условия и цель матерого разработчика писать легко масштабируемый код. Пусть даже приложение будет трижды крутым, но если каждый допил будет сопровождаться головной болью, то рано или поздно от поддержки неуклюжей поделки придется отказаться.
Сразу вспоминаю пример из своей практики. Однажды мне пришлось участвовать в доработке функционала самописной erp. Вроде бы и код был написан неплохо, но отсутствие четкой архитектуры и сильная связанность всех компонентов быстро разбили мои юношеские грезы. В итоге уверенность написать все по-быстрому и получить бабло, конвертнулась в несколько недель интимных взаимоотношений с деббагером. Новый функционал предательски конфликтовал со старым кодом и порождал настоящий хаос. Баги размножались как кролики, а в моих глазах угасал позитив. Жаль, что тогда я не знал о методике под странным названием «внедрение зависимостей». А ведь она могла сохранить голову от многих шишек, набитых по неопытности.
Хорошая архитектура приложения подразумевает отсутствие сильной связи между составляющими ее компонентами. Чем меньше связь, тем ниже вероятность сломать работающий код при добавлении нового.
Как я уже сказал, одним из способов ослабить связь компонентов приложения -воспользоваться методикой под названием «внедрение зависимостей» (Dependency Injection, DI). Применяя DI, разработчик не указывает конкретных зависимостей, а работает сугубо с интерфейсами. Необходимая реализация объектов будет внедрена в приложение уже во время его работы, исходя из заданных настроек конфигурации.
Не переживай, если до этого ты ни разу не применял технику внедрения зависимостей. Скупой текст определения может загнать в ступор, но на практике все обязательно встанет на свои места.
Перед тем как приступать к рассмотрению практических примеров, нам необходимо определиться с фреймворком, посредством которого будем реализовать внедрение зависимостей. Для .NET их написано достаточно много. Даже Microsoft успел зарелизить свой вариант в лице Unity. Я его попробовать еще не успел, т.к. для своих проектов предпочитаю применять хорошо себя зарекомендовавший фреймворк Ninject. Он прост в использовании, а его функционал с лихвой покрывает многообразие задач, возникающих в типичных web-проектах.
Для практики я решил не создавать бездушных глянцевых проектов, в которых всегда все работает как надо. Вместо этого я отважился на допил примера, рассмотренного в предыдущей статье. Подними исходники примера в VisualStudio (можешь взять с нашего диска) и при помощи команды “Install-Package Ninject” добавь фреймворк Ninject к своему проекту.
Теперь займемся модификацией примера. У нас есть несколько моделей и контекст для работы с Entity Framework. В настоящий момент, получение всех данных из базы реализовано напрямую, т.е. в определенном методе контроллера происходит обращение к контексту BugTrackerContext. Так делать крайне нежелательно, поскольку мы крепко связываем контроллер с определенным контекстом. Попробуем разрушить эту прочную связь и сделаем так, чтобы контроллер не фига не знал о существовании BugTrackerContext.
Для начала опишем новый интерфейс IBugTrackerRepository. Разместим его в директории Models/Abstract (в реальном приложении для такой цели лучше завести отдельный проект). Опишем в рамках интерфейса методы для получения из БД сущностей, определенных в проекте. Полный код интерфейса описан в листинге 1.
Листинг №1. Код интерфейса IBugTrackerRepository
IQueryable<Category> GetCategories { get; }
IQueryable<Status> GetStatuses { get; }
IQueryable<User> GetUsers { get; }
IQueryable<Ticket> GetTickets { get; }
bool CreateCategory(Category category);
bool EditCategory(Category category);
bool RemoveCategory(int CategoryId);
bool CreateTicket(Ticket ticket);
....
В интерфейсе я определил действия классов, которые будут его реализовать. Тут все достаточно просто – есть методы для добавления новых сущностей в базу, а есть методы для выборки из базы.
Заострять внимание на деталях реализации класса на основе данного интерфейса я не стану. Ибо там тот же самый код, который до этого был прописан в контроллере Home. Можешь слямзить его самостоятельно оттуда.
Будем считать, что интерфейс и его реализация в виде класса BugTrackerRepository у нас готовы. Пора переходить к главной теме, ради которой были проделаны эти телодвижения – знакомству с библиотекой Ninject и методикой внедрения зависимостей.
Перед тем как ты возьмешь в руки шприц, мне хотелось бы заострить твое внимание на рассмотрении популярных способов внедрения зависимостей. Перечисленные ниже варианты далеко не единственные. В литературе по DI (кстати, ссылку на свежую книгу по теме, ищи во врезке) приводятся более экзотические примеры внедрения зависимостей. Однако, на практике они встречаются не сильно часто и применять их стоит только в особых случаях. Запомни, если есть возможность обойтись наиболее распространенным подходом, то лучше выбрать именно его.
Итак, из наиболее популярных способов внедрения зависимостей стоит выделить:
На практике мы попробуем реализовать внедрение зависимости через конструктор. Как я уже сказал, этот способ достаточно распространен и прост для понимания. Напомню, наша цель добиться, чтобы в контролере Home (а также во всех других) мы работали не с BugTrackerContext, а с интерфейсом IBugTrackerRepository.
Поскольку контроллер Home не единственная наша цель мы должны действовать глобально - вторгнемся в святая из святых и напишем свою реализацию фабрики контроллеров. Звучит немного пугающе, но на практике от нас требуется всего лишь реализовать производный класс от System.Web.Mvc.DefaultControllerFactory. Готовый код реализации я привел во втором листинге.
Листинг №2. Реализация собственной фабрики контроллеров
public class NinjectControllerFactory : DefaultControllerFactory
{
private IKernel ninjectKernel;
public NinjectControllerFactory()
{
ninjectKernel = new StandardKernel();
AddBindings();
}
private void AddBindings()
{
//Здесь будем определять все наши привязки
}
protected override IController GetControllerInstance(
System.Web.Routing.RequestContext requestContext,
Type controllerType)
{
return controllerType == null ? null :
(IController)ninjectKernel.Get(controllerType);
}
}
В коде класса, приведенного во втором листинге, описана инициализация ядра фреймворка Ninject. Оно будет отвечать за обслуживание запросов от классов контроллеров. Метод AddBindings() выполняет роль регистратора привязок Ninject. Пока здесь не описано ни одной привязки, но вопрос времени.
Чтобы фреймворк (сейчас речь об MVC) подхватил нашу фабрику контроллеров ее надо зарегистрировать. Это можно сделать в методе Application_Start(), класса MvcApplication(см. файл Global.asax):
ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory());
У нас все готово для внедрения зависимости, чистоты эксперимента напишем еще один класс, реализующий интерфейс IBugTrackerRepository. Он поможет нам лучше прочувствовать выгоду от применения DI.
Фейковый репозиторий будет генерировать запрашиваемые данные на лету, а не выбирать из реальной базы (см. код в третьем листинге). Получается, что данная реализация интерфейса IBugTrackerRepository выполняет роль «заглушки». С его помощью мы можем разрабатывать дальнейший функционал приложения, не заморачиваясь на разработку класса, взаимодействующего с реальным хранилищем.
Листинг 3. Фейковый репозиторий
public class MyFakeRepository : IBugTrackerRepository
{
public IQueryable<Ticket> Tickets
{
get
{
Ticket[] tickets = {
new Ticket() { Category = new Category() {Title ="Тест"},
Date = DateTime.Now, Description = "Проблемы с принтером",
Status = new Status() { Title = "Открыто"},
Title = "Не печатает принтер",
User = new User() { Email = "antonov.igor.khv@gmail.com", FirstName = "Igor", LastName = "Antonov"}
}
};
return tickets.AsQueryable();
}
}
…
Попробуем все это дело заюзать на практике. Снабдим контроллер Home конструктором (см. листинг 4) и слегка подправим общий код. В описании конструктора определен один входной параметр типа IBugTrackerRepository, т.е. тип соответствующий интерфейсу ранее объявленному нами интерфейсу. Во время инициализации контроллера, переданное в параметре значение, записывается в свойство db. Через объект, определенный в этом свойстве, мы будем взаимодействовать с хранилищем с данными.
Листинг 4. Код конструктора контроллера Home
private IBugTrackerRepository db;
public HomeController(IBugTrackerRepository bugTrackerRepository)
{
this.db = bugTrackerRepository;
}
На данный стадии проект уже почти готов к запуску. Остается лишь добавить в метод AddBindings() привязку для фреймворка Ninject(). Сначала попробуем заюзать вместо реальной базы наш фейковый репозиторий. Для этого добавляем вот такую привязку:
ninjectKernel.Bind<IBugTrackerRepository>().To<MyFakeRepository>();
Привязка говорит, что при запросе типа IBugTrackerRepository Ninject будет автоматически возвращать тип MyFakeRepository. Попробуй запустить проект и убедиться, что данные берутся не из базы, а из MyFakeRepository. Убедился? Отлично, а теперь попробуй оперативно переключится вновь на реальную базу. Для этого всего лишь измени привязку на BugTrackerRepository и пересобери проект.
Логи – наше все
Уж мы с тобой знаем, что безбажных программ не бывает, а идеального кода не существует. Ошибки возможны всегда и везде, даже если большая часть кода покрыта толстым слоем юнит-тестов.
Допустить ошибку и не предусмотреть обработку некорректного действия всегда присутствует и этого не нужно стесняться. Как говорится: «Не ошибается только тот, кто ничего не кодит».
Важно своевременно узнать о подобных ситуациях и пресечь их на корню. В этом нелегком деле хорошо помогает правильная организация системы логирования. Чем больше ты хочешь выудить полезной информации для отладки, тем основательней стоит подойти к ведению логов. Облегчить ведения логов ASP .NET MVC приложения помогает пакет NLog (“Install-Package NLog”).
Сразу после установки NLog необходимо его сконфигурировать посредством внесения изменений в файл настроек приложения Web.config:
<section name="nlog" type="NLog.Config.ConfigSectionHandler, NLog" /> <nlog autoReload="true" xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <variable name="logDirectory" value="${basedir}/logs/${shortdate}" /> <targets> <target name="debugLog" xsi:type="File" fileName="${logDirectory}/debug.txt" /> </targets> <rules> <logger name="*" level="Debug" writeTo="debugLog" /> </rules> </nlog>
В конфигурационном файле определяется общая директория для хранения логов, а также разграничиваются типы событий по разным файлам. Например, отладочные сообщения будут помещаться прямиком в файл debug.txt.
Что касается самой записи отладочных сообщений, то она выполняется примерно так:
NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
logger.Debug("Это информация для дебага");
При изучении реальных логов, сформированных пакетом NLog, рекомендую рекомендую обзавестись небольшой тулзой под названием Log2Console (http://goo.gl/0tpoH). Рулить логами с ее помощью одно удовольствие, т.к. она группирует все события по типам (debug, info и т.д.) и избавляет от необходимости каждый раз выбирать обновившийся лог-файл. Достаточно указать файлы для мониторинга и log2Console будет любезно отображать из них обновленное содержимое.
Регистрация и авторизация
В 99% web-проектов требуется система разграничения пользователей или говоря иными словами – регистрация и авторизация. Читатели нашего журнала знают, какие последствия сулит в этой области говно код. Не стоит лишний раз испытывать судьбу и изобретать очередной велосипед с дуршлагом. К тому же, Microsoft позаботилась о нас и приготовила классный инструмент для решения этой не совсем тривиальной задачи.
До появления 4-й версии фреймворка MVC, стандартным решением был старичок ASP NET Membership System. Он всегда жестко критиковался ASP .NET разработчиками за полное отсутствие гибкости и несоответствия современным реалиям. Утихомирить священные споры и решить часть проблем позволил новый провайдер авторизации от Microsoft - SimpleMembership.
Он хорошо адаптируется под различные проекты, из коробки заряжен поддержкой oAuth, понимает роли и всегда готов подвергнуться допилам со стороны разработчиков. Попробуем снабдить багтрекер системой авторизации и регистрации на основе SimpleMembership.
Не стану подробно заострять внимание на деталях добавления регистрационной формы. Это ты сможешь сделать сам, не маленький уже. Свое внимание я акцентирую на последовательность действий, необходимых для подключения SimpleMembership к нашему проекту.
Процесс интеграции начинаем с подключения к своему проекту двух сборок: WebMatrix.Data и WebMatrix.WebData. Они доступны в нескольких версиях, но нас интересуют максимально свежие. Далее нам предстоит определиться с таблицей, в которой будут храниться связки ID пользователя + login. SimpleMembership не заставляет придерживаться каких-то жестких правил к определению структуры таблицы или ее имени (тоже самое относится и к колонкам). Главное, чтобы в ней были определены графы для хранения ID и логина. Не уподобляясь извращенным фантазиям, я решил остановиться на вполне стандартных именах: Users (имя таблицы), UserID (колонка с уникальным идентификатором) и Email (колонка для хранения имени пользователя).
Чтобы донести эту информацию до SimpleMembership, в методе Application_Start пришлось дописать строку, выполняющую первоначальную инициализацию:
WebSecurity.InitializeDatabaseConnection("BugTrackerContext", "Users", "UserID", "Email", autoCreateTables: true);
В параметрах к методу InitializeDatabaseConnection() я передаю: имя соединения с БД (в пример используется единственный контекст), имя таблицы для хранения списка пользователей, название колонки с уникальным идентификатором и логином. Последний параметр говорит, что SimpleMembership’у разрешено автоматически создать все необходимые таблицы (если их еще нет).
Теперь отстает лишь сообщить нашему приложению, что в качестве провайдера авторизации мы хотим использовать именно SimpleMembership. Для этого в конфигурационном файле добавляем дополнительную секцию:
<membership defaultProvider="SimpleMembershipProvider"> <providers> <clear /> <add name="SimpleMemberShipProvider" type="WebMatrix.WebData.SimpleMembershipProvider, WebMatrix.WebData" /> </providers>1 </membership>
Этих действий хватит, чтобы подключить SimpleMembership к своему проекту. Дальше остается создать форму регистрации и входа в которых заюзать один из соответствующих методов класса WebSecurity:
АвтоМэппинг наше все
Каждая новая фича раздувает наш проект новыми классами. Там надо сделать модель представления, а потом данные из этой модели перегнать в класс, описывающий предметную модель. Постоянно приходится перегонять данные из одних объектов в другие. Делать эту рутинную операцию ручками, аккуратно заполняя каждое свойство, чересчур утомительно. Без резвого помощника тут ну никак не обойтись. К счастью, далеко за ним ходить не нужно, AutoMapper к твоим услугам.
Добавляем пакет к проекту стандартным образом: Install-Project Automapper. Для использования мэппера создаем карту типов, которые будем мапить, а затем, в нужном месте юзаем единственный метод Map(). В коде это будет выглядеть примерно так:
Mapper.CreateMap<User, UserView>();
Mapper.CreateMap<UserView, User>();
Var User = (User)Mapper.Map(UserView, typeof(UserView), typeof(User));
Этот кусочек кода я выдрал из системы регистрации на основе SimpleMembership (опять отправляю тебя к исходнику). Пользователь вводит свои регистрационные данные в модель представления (UserView), а уже из нее они маппятся в предметную область User. Применительно к нашему примеру эффективность от использования AutoMapper не столь очевидна, т.к. свойств у обоих моделей мало. Выгода проявится, когда в моделях будет присутствовать по 10-20 свойств и их нужно быстренько перегнать.
В Json и обратно
Времена, когда вся "динамика" web-сайта крутилась лишь на сервере безвозвратно прошли. Никто не хочет генерировать сложную страницу целиком по каждому чиху пользователя. Вместо этого, большая часть обменов между клиентом и сервером происходит в фоновом режиме при помощи Ajax.
Казалось бы, какие тут могут быть подводные камни? Подготовил данные на клиенте в JSON'е и отправил их запросом серверу при помощи той же библиотеки jQuery Однако, проблема все же есть. На сервере надо как-то работать с полученными данными и тут без хорошей библиотеки, умеющей работать с JSON, никуда.
Для .NET существует несколько подобных разработок (в ASP .NET MVC4 появилось встроенное средство), но я по привычке использую пакет JSON.net. Делает свою работу он шустро и качественно, а это самое главное. Подключение json.net к своему проекту выполняется стандартным способом: Install-Package Json.net. Ну а дальше у нас появляется возможность конвертировать данные в формат JSON и обратно:
//Конвертируем Ticket в Json
var jsonTicket = JsonConvert.SerializeObject(Ticket);
//делаем обратное преобразование.
Ticket ticket = JsonConvert.DeserializeObject<Ticket>(jsonTicket);
И это еще не все
Рассмотренные в статье пакеты далеко не единственные в своем роде. Менеджер пакетов NuGet готов предложить сотни готовых решений на все случаи жизни. Не бойся качать готовый код и экспериментировать с ним. Помни, что для большинства типичных задач уже созданы элегантные решения и лучше юзать именно их, а не вступать в ряды унылых сборщиков велосипедов. Удачных тестов и качественных пакетов на пути!
Попробуй сам
Полезные ссылки
Статья опубликована в журнале "Хакер" (http://xakep.ru). Ноябрь 2013 г.
Ссылка на журнал: http://goo.gl/br16bh