Как написать простой блог с помощью Asp .Net MVC, Nhibernate и Nineject.Часть 1
Cоздание базовой инфраструктуры.
Опишем задачи которые нам необходимо реализовать в этой части:
Задача 1.Выводить последние пост в блоге
Задача 2.Выводить посты определенной категории,
Задача 3.Выводить посты на основе тега
Задача 4.Поиск постов
Задача 5.Выводить пост полностью
Задача 6.Выводить посты определенной категории, в виджете
Задача 7.Выводить посты на основе тега в виджет
Задача 8.Выводить последние пост в виджете
Задача 1. Выводить последние пост в блоге
Чтобы выполнить эту задачу нам необходимо сделать выборку постов блога из базы данных и отобразить их в представлении.
Эта задача достаточно большая, поэтому давайте разобьем ее на несколько более мелких подзадач, которые будет легче выполнить по отдельности
- Создать базовое решение (solution)
- Создать доменные классы
- Настроить Fluent NHibernate и NHibernate
- Создать классы сопоставлений, классы доступа к данным и методы
- Настроить Ninject для проекта JustBlog.Core
- Настроить Ninject для проекта MVC
- Создать контроллер и действия
- Создать представление
4.1 Создание базового решения
Создайте проект MVC 4 web application и назовите его JustBlog.
В окне «Select a template» выберите «Empty template».
Создайте библиотеку классов (class library) и назовите ее JustBlog.Core. Рекомендуется хранить доменные классы и компоненты доступа к данным в отдельных проектах, так нам будет легче управлять приложением в плане разработки, тестирования и развертывания. Не забудьте добавить ссылку на JustBlog.Core в JustBlog.
Решение должно выглядеть так, как показано ниже.
4.2 Создание доменных классов
В проекте JustBlog.Core создайте новую папку под названием Objects, чтобы разместить в ней классы домена. Для нашего блога нам нужно создать три класса: Post, Category и Tag. Каждый Post принадлежит одной Category, но его можно пометить множеством Tags. Между Post и Category отношения многие-к-одному и между Post и Tag отношения многие-ко-многим.
Отношения между классами показаны на скриншоте ниже
Ниже показан наш класс Post
namespace JustBlog.Core.Objects
{
public class Post
{
public virtual int Id
{ get; set; }
public virtual string Title
{ get; set; }
public virtual string ShortDescription
{ get; set; }
public virtual string Description
{ get; set; }
public virtual string Meta
{ get; set; }
public virtual string UrlSlug
{ get; set; }
public virtual bool Published
{ get; set; }
public virtual DateTime PostedOn
{ get; set; }
public virtual DateTime? Modified
{ get; set; }
public virtual Category Category
{ get; set; }
public virtual IList Tags
{ get; set; }
}
}
Большинство свойств не требуют пояснений. Свойство UrlSlug является альтернативным для свойства Title и используется при адресации.
Например, у нас есть пост с заголовком «Advanced Linq in C#», опубликованный в августе 2010 года, мы создаем для него URL-адрес http//localhost/archive/2010/8/Advanced Linq in C#. Заголовок поста может содержать специальные символы (в данном примере »#») и не все серверы могут обрабатывать запросы, содержащие специальные символы. Вместо использования свойства Title непосредственно в URL, мы будем использовать некоторый альтернативный текст, который похож на заголовок поста и брать мы будем его из свойства URL Slug.
В вышеописанном случае, вместо использования «Advanced Linq in C#» в URL мы собираемся использовать дружественный текст (slug) «advanced_linq_in_csharp», поэтому адрес будет http//localhost/archive/2010/8/advanced_linq_in_csharp. В части 3 мы узнаем, как автоматически создать slug из заголовка поста.
URL Slug
URL Slug является SEO — и user-дружеcтвенной частью строки в составе URL для идентификации, описания и получения доступа к ресурсу. Часто заголовок страницы статьи является подходящим кандидатом для этого.
Cвойство Мета используется для хранения метаданных описаний поста и используется для SEO. Все свойства помечены как виртуальные, потому как NHibernate во время выполнения создает прокси-сервер и ему для работы необходимо, чтобы все свойства этого класса были виртуальными.
Классы Category и Tag, очень простые и показана ниже. Мы используем свойство UrlSlug по той же причине, которую мы обсуждали выше.
namespace JustBlog.Core.Objects
{
public class Category
{
public virtual int Id
{ get; set; }
public virtual string Name
{ get; set; }
public virtual string UrlSlug
{ get; set; }
public virtual string Description
{ get; set; }
public virtual IList Posts
{ get; set; }
}
}
namespace JustBlog.Core.Objects
{
public class Tag
{
public virtual int Id
{ get; set; }
public virtual string Name
{ get; set; }
public virtual string UrlSlug
{ get; set; }
public virtual string Description
{ get; set; }
public virtual IList Posts
{ get; set; }
}
}
4.3 Настройка Fluent NHibernate и NHibernate
Для доступа к базе данных мы будем использовать NHibernate вместе с Fluent NHibernate. NHibernate-это ORM инструмент наподобие Entity Framework, где отношения между классами и таблицами базы данных сопоставляются через XML-файлы. Fluent NHibernate-это расширение для NHibernate который заменяет сопоставление с помощью XML-файлов сопоставлением с помощью специальных классов. Сопоставление через классы гораздо проще, чем через XML-файлы.
Мы можем легко добавить ссылки на сборки NHibernate и Fluent NHibernate с помощью Nuget Package Manager Console. Откройте Package Manager из меню Tools → Library Package Manager → Package Manager Console.
В консоли выполните следующую команду.
PM> Install-Package FluentNHibernate
Если установка прошла успешно вы увидите в папке References проекта JustBlog.Core следующие сборки.
FluentNHibernate
NHibernate
Iesi.Collections
4.4 Создание классов сопоставлений, классов доступа к данным и методов
Следующее, что мы должны сделать, это создать необходимое классы сопоставлений (mapping classes). Класс сопоставления используется для того, чтобы класс и его свойства сопоставить таблице и ее столбцам. Создайте в проекте JustBlog.Core новую папку под названием Mappings, чтобы хранить все классы сопоставлений.
Для создания класса сопоставления мы должны унаследовать его от обобщенного класса ClassMap фреймворка Fluent NHibernate. Все сопоставления должны выполняться в конструкторе.
Класс сопоставления для класса Post.
using FluentNHibernate.Mapping;
using JustBlog.Core.Objects;
namespace JustBlog.Core.Mappings
{
public class PostMap: ClassMap
{
public PostMap()
{
Id(x => x.Id);
Map(x => x.Title).Length(500).Not.Nullable();
Map(x => x.ShortDescription).Length(5000).Not.Nullable();
Map(x => x.Description).Length(5000).Not.Nullable();
Map(x => x.Meta).Length(1000).Not.Nullable();
Map(x => x.UrlSlug).Length(200).Not.Nullable();
Map(x => x.Published).Not.Nullable();
Map(x => x.PostedOn).Not.Nullable();
Map(x => x.Modified);
References(x => x.Category).Column("Category").Not.Nullable();
HasManyToMany(x => x.Tags).Table("PostTagMap");
}
}
}
Метод расширения Id представляет имя свойства, которое необходимо установить в качестве первичного ключа для столбца таблицы.
Метод расширения Map используется, чтобы сопоставить свойство со столбцом таблицы. Во время сопоставления свойств мы можем задать размер столбца, указать позволяет ли он хранить значения NULL или задать другие спецификации.
Например:
Map(x => x.ShortDescription).Length(5000).Not.Nullable();
Если сгенерированное имя столбца отличается от имени свойства то мы должны передать имя столбца, с помощью метода расширения Column.
Map(x => x.Title).Column("post_title")
Метод References используется для представления связи многие-к-одному между Post и Category с помощью внешнего ключевого столбца Category таблицы Post. Метод HasManyToMany используется для представления связи «многие-ко-многим» между Post и Tag через промежуточную таблицу PostTagMap. Вы можете больше узнать о Fluent NHibernate и его методох расширений отсюда.
По умолчанию, Fluent NHibernate предполагает, что имя таблицы точно такое же, как имя класса, а имена столбцов точно такие же, как имена свойств. Если имя таблицы отличается, то мы должны сопоставить таблицу с классом, с помощью метода расширения Table.
Например:
Table("tbl_posts");
Классы сопоставлений для Category и Tag точно такие же, за исключением их отношений с Post. Category имеет отношение с Post один-ко-многим, в то время как Tag имеет с Post отношение многие-ко-многим.
namespace JustBlog.Core.Mappings
{
public class CategoryMap: ClassMap
{
public CategoryMap()
{
Id(x => x.Id);
Map(x => x.Name).Length(50).Not.Nullable();
Map(x => x.UrlSlug).Length(50).Not.Nullable();
Map(x => x.Description).Length(200);
HasMany(x => x.Posts).Inverse().Cascade.All().KeyColumn("Category");
}
}
}
Метод расширения Inverse (), делает другую сторону отношений (relationship) ответственной за сохранение.
Метод расширения Cascade.All () запускает каскадный вызов событий (вниз по иерархии) в сущностях коллекции (поэтому при сохранении категории, также будут сохраняться и посты).
namespace JustBlog.Core.Mappings
{
public class TagMap: ClassMap
{
public TagMap()
{
Id(x => x.Id);
Map(x => x.Name).Length(50).Not.Nullable();
Map(x => x.UrlSlug).Length(50).Not.Nullable();
Map(x => x.Description).Length(200);
HasManyToMany(x => x.Posts).Cascade.All().Inverse().Table("PostTagMap");
}
}
}
Мы будем использовать шаблон Repository для доступа к базе данных. Мы используем этот шаблон, чтобы отделить код доступа к данным от контроллеров, и это поможет нам упростить модульное тестирование (unit testing) наших контроллеров. Ядро шаблона репозитория — это интерфейс, который содержит описания всех методов доступа к данным.
В проекте JustBlog.Core создадим интерфейс IBlogRepository с двумя методами.
using JustBlog.Core.Objects;
namespace JustBlog.Core
{
public interface IBlogRepository
{
IList Posts(int pageNo, int pageSize);
int TotalPosts();
}
}
Метод Posts используется для возвращения последних опубликованных постов разбитых на страницы в соответствии с переданными ему значениям. Метод TotalPosts возвращет общее количество опубликованных постов. Далее мы наполним интерфейс другими методами.
В проекте JustBlog.Core класс с именем BlogRepository и реализуем интерфейс.
using JustBlog.Core.Objects;
using NHibernate;
using NHibernate.Criterion;
using NHibernate.Linq;
using NHibernate.Transform;
using System.Collections.Generic;
using System.Linq;
namespace JustBlog.Core
{
public class BlogRepository: IBlogRepository
{
// NHibernate object
private readonly ISession _session;
public BlogRepository(ISession session)
{
_session = session;
}
public IList Posts(int pageNo, int pageSize)
{
var posts = _session.Query()
.Where(p => p.Published)
.OrderByDescending(p => p.PostedOn)
.Skip(pageNo * pageSize)
.Take(pageSize)
.Fetch(p => p.Category)
.ToList();
}
var postIds = posts.Select(p => p.Id).ToList();
return _session.Query()
.Where(p => postIds.Contains(p.Id))
.OrderByDescending(p => p.PostedOn)
.FetchMany(p => p.Tags)
.ToList();
}
public int TotalPosts()
{
return _session.Query().Where(p => p.Published).Count();
}
}
}
Все обращения к базе данных должны осуществляться через объект NHibernate ISession. Когда мы читаем коллекцию постов через ISession, зависимости Category и Tags по умолчанию не заполняются. Методы Fetch и FetchMany используются, чтобы сообщить NHibernate заполнить их немедленно.
В методе Posts, чтобы получить посты, мы дважды запрашиваем базу данных, потому что мы не можем использовать FetchMany вместе с методами Skip и Take в Linq запросе. Итак, сначала мы выбираем все посты, затем с помощью их ID мы снова выбираем посты, чтобы получить их теги. Пожалуйста, посмотрите эту ветку обсуждения для получения дополнительных сведений об этой проблеме.
NHibernate ISession
ISession это интерфейс менеджера сохранения данных (persistence manager), который используется для хранения и извлечения объектов из базы данных.
Persistent означает, что этот экземпляр следует реализовать таким образом, чтобы он сохранялся после завершения процесса, его породившего. В отличие от transient, означающего, что этот экземпляр объекта временный — он прекращает свое существование после завершения породившего его процесса. (прим.переводчика).
4.5 Настроика Ninject для проекта JustBlog.Core
Внедрение зависимости (DI) позволяет избежать создания экземпляров конкретных реализаций зависимостей внутри класса. Эти зависимости обычно вводят в класс через конструктор, но иногда через свойства. Одним из основных преимуществ внедрения зависимостей является модульное тестирование и мы увидим это когда напишем юнит-тесты для контроллеров в части 3.
Есть много фреймворков для упрощения внедрения зависимостей, как Castle Windsor, Unity, Autofac, StructureMap, Ninject и т. д. Мы выбрали Ninject, потому что он прост в использовании.
Установим Ninject в проект JustBlog.Core, выполнив следующие команды в Package Manager Console.
PM> Install-Package Ninject
PM> Install-Package Ninject.Web.Common
После успешного выполнения команд, мы увидим, что сборки Ninject и NinjectWeb.Common добавились в проект. Вместе со сборками также добавился файл NinjectWebCommon.cs в папку App_Start. В ближайшее время я объясню, почему нам нужно установить расширение Ninject.Web.Common.
Мы можем настроить Ninject двумя способами, либо с помощью Global.asax.cs либо через файл App_Start. Мы будем использовать первый подход, поэтому, пожалуйста, удалите файл NinjectWebCommon.cs из папки App_Start, а также удалите ненужные ссылки на сборки WebActivator и Microsoft.Web.Infrastructure из проекта.
Основные функциональные возможности любого DI-фреймворка — это связывание интерфейсов с конкретными реализациями этих интерфейсов. Сопоставление интерфейса с конкретной реализации называется привязка (binding). Мы можем сгруппировать набор привязок связанных с конкретным модулем Ninject с помощью Ninject Modules. Все привязки и модули загружены в основной компонент Ninject ядра Kernel. Всякий раз, когда приложению необходимо экземпляр конкретного класса, который реализует интерфейс, Kernel, предоставляет его приложению.
Ninject Module
Модуль Ninject используется для группировки сопостовлений/привязок, относящиеся к конкретному модулю в одном классе.
Класс BlogRepository имеет зависимость с Nibernate ISession. Чтобы создать экземпляр ISession нам нужен другой Nibernate интерфейс под названием ISessionFactory. Давайте создадим в проекте JustBlog.Core ninject-модуль класса с именем RepositoryModule, который свяжет оба этих интерфейса.
Создаваемыq Ninject-модуль должен наследоваться от абстрактного класса NinjectModule и реализовать метод Load. В методе Load, мы свяжем оба интерфейса с методами, с помощью метода Bind.
Метод Bind применяется для связывания интерфейса с классом, который его реализует.
Например:
Bind().To();
Также мы можем сопоставить интерфейс с методом, который создает и возвращать реализацию интерфейса.
Bind().ToMethod(c => {
var foo = new Foo();
return foo;
});
NHibernate ISessionFactory
В отличие от ISession нам нужен один экземпляр ISessionFactory во всем приложении.
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using JustBlog.Core.Objects;
using NHibernate;
using NHibernate.Cache;
using Ninject;
using Ninject.Modules;
using Ninject.Web.Common;
namespace JustBlog.Core
{
public class RepositoryModule: NinjectModule
{
public override void Load()
{
Bind()
.ToMethod
(
e =>
Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2008.ConnectionString(c =>
c.FromConnectionStringWithKey("JustBlogDbConnString")))
.Cache(c => c.UseQueryCache().ProviderClass())
.Mappings(m => m.FluentMappings.AddFromAssemblyOf())
.ExposeConfiguration(cfg => new SchemaExport(cfg).Execute(true, true, false))
.BuildConfiguration()
.BuildSessionFactory()
)
.InSingletonScope();
Bind()
.ToMethod((ctx) => ctx.Kernel.Get().OpenSession())
.InRequestScope();
}
}
}
Сцепленные методы расширения выглядят немного запутанными! Ниже перечислены задачи, которые мы решаем с помощью этих методов.
а. Настраиваем строку подключения к базе данных (Database)
б. Настраиваем провайдера для кэширования запросов (Cache)
в. Указываем сборку, в которой существуют классы домена и классы сопоставления (Mappings)
д. Просим NHibernate создать таблицы из классов (ExposeConfiguration)
Fluently.Configure()
.Database(MsSqlConfiguration.MsSql2008.ConnectionString(c =>
c.FromConnectionStringWithKey("JustBlogDbConnString")))
.Cache(c => c.UseQueryCache().ProviderClass())
.Mappings(m => m.FluentMappings.AddFromAssemblyOf())
.ExposeConfiguration(cfg => new SchemaExport(cfg).Execute(true, true, false))
.BuildConfiguration()
.BuildSessionFactory()
Есть много расширений, доступных в Ninject и одним из них является Ninject.Web.Common, который содержит некоторые общие функциональные возможности, необходимые WebForms и MVC приложениям. Нам нужно сделать так, чтобы пока выполнялся запрос Ninject возвращал один и тот же экземпляр ISession. Метод расширения InRequestScope (), определенный в пространстве имен Ninject.Web.Common, сообщает Ninject о том, что для каждого HTTP-запроса, получаемого ASP.NET, должен создаваться только один экземпляр класса. Метод расширения InSingletonScope () создает одиночный экземпляр (singleton), который доступен во всем приложении.
4.6 Настроика Ninject для проекта MVC
Все обращения к базе данных из контроллеров выполняются через интерфейс IBlogRepository. Чтобы внедрить экземпляр класса, который реализует IBlogRepository в контроллер нужно в MVC-приложении настроить Ninject. Есть расширение (Ninject.Mvc3) специально созданное для поддержки приложений MVC.
Установим Ninject.Mvc3 в нашем проекте JustBlog, выполнив следующую команду.
PM> Install-Package Ninject.Mvc3
После успешного выполнения команд в MVC проекте добавятся следующие сборки:
Ninject
Ninject.Web.Common
Ninject.Web.Mvc
Удалите файл NinjectWebCommon.cs из папки App_Start. Наследуйте наш глобальный класс приложения от класса NinjectHttpApplication и переопределите метод CreateKernel.
Модифицируйте файл Global.asax.cs так как показано ниже.
using Ninject;
using Ninject.Web.Common;
using System.Web.Mvc;
using System.Web.Routing;
namespace JustBlog
{
public class MvcApplication : NinjectHttpApplication
{
protected override IKernel CreateKernel()
{
var kernel = new StandardKernel();
kernel.Load(new RepositoryModule());
kernel.Bind().To();
return kernel;
}
protected override void OnApplicationStarted()
{
RouteConfig.RegisterRoutes(RouteTable.Routes);
base.OnApplicationStarted();
}
}
}
В методе CreateKernel, мы создаем и возвращаем экземпляр StandardKernel типа IKernel. IKernel-это ядро приложения, где мы указываем наши привязки и когда нам нужен экземпляр сопоставленный с интерфейсом он предоставляет нам его.
С помощью экземпляра класса StandardKernel мы делаем пару важных вещей. Во-первых, мы загружаем экземпляр нашего модуля Repository, который содержит все привязки, связанные с интерфейсами NHibernate и затем связываем интерфейс IBlogRepository непосредственно с его реализации BlogRepository. Ну и наконец, в конце примера, код запуска приложения переехал в метод OnApplicationStarted.
Вот и все о конфигурации Ninject в приложение. В следующей части мы начнем работать над контроллерами и представлениями.