Подготовка
Традиционно, если вы еще не работали с ASP.NET Core, то здесь есть ссылки на все, что для этого понадобится.
Запускаем Visual Studio, создаем новое веб-приложение:
Веб-приложение готово. При желании его можно запустить.
Приступаем
Модели
Начнем с моделей. Вынесем их классы в отдельный проект — библиотеку классов AspNetCoreStorage.Data.Models:
Добавим класс нашей единственной модели Item:
public class Item
{
public int Id { get; set; }
public string Name { get; set; }
}
Для нашего примера этого хватит.
Абстракции взаимодействия с хранилищем
Теперь перейдем непосредственно к взаимодействию с хранилищем, которое в нашем веб-приложении будет реализовано с применением двух шаблонов проектирования — Единица работы и Репозиторий. Имплементация этих шаблонов упрощенно означает, что взаимодействие с хранилищем в рамках одного запроса будет гарантированно производиться в едином контексте хранилища, а для каждой модели будет создан отдельный репозиторий, содержащий все необходимые методы для манипуляций с ней.
Для обеспечения возможности простого переключения между различными реализациями взаимодействия с хранилищем, наше веб-приложение не должно использовать какую-то конкретную реализацию напрямую. Вместо этого все взаимодействие с хранилищем должно производиться через слой абстракций. Опишем его в библиотеке классов AspNetCoreStorage.Data.Abstractions (создадим соответствующий проект).
Для начала добавим интерфейс IStorageContext без каких-либо свойств или методов:
public interface IStorageContext
{
}
Классы, реализующие этот интерфейс, будут непосредственно описывать хранилище (например, базу данных со строкой подключения к ней).
Далее, добавим интерфейс IStorage. Он содержит два метода — GetRepository и Save:
public interface IStorage
{
T GetRepository<T>() where T : IRepository;
void Save();
}
Этот интерфейс описывает реализацию шаблона проектирования Единица работы. Объект класса, реализующего этот интерфейс, будет единственной точкой доступа к хранилищу и должен существовать в единственном экземпляре в рамках одного запроса к веб-приложению. За создание этого объекта у нас будет отвечать встроенный в ASP.NET Core DI.
Метод GetRepository будет находить и возвращать репозиторий соответствующего типа (для соответствующей модели), а метод Save — фиксировать изменения, произведенные всеми репозиториями.
Наконец, добавим интерфейс IRepository с единственным методом SetStorageContext:
public interface IRepository
{
void SetStorageContext(IStorageContext storageContext);
}
Очевидно, что этот интерфейс описывает классы репозиториев. В момент запроса репозитория объект класса, реализующего интерфейс IStorage, будет передавать единый контекст хранилища в возвращаемый репозиторий с помощью метода SetStorageContext, чтобы все обращения к репозиторию производились в рамках этого единого контекста, как мы говорили выше.
На этом общие интерфейсы описаны. Теперь добавим интерфейс репозитория нашей единственной модели Item — IItemRepository. Этот интерфейс содержит лишь один метод — All:
public interface IItemRepository : IRepository
{
IEnumerable<Item> All();
}
В реальном веб-приложении здесь также могли бы быть описаны методы Create, Edit, Delete, какие-то методы для извлечения объектов по различным параметрам и так далее, но в нашем упрощенном примере в них необходимости нет.
Конкретные реализации взаимодействия с хранилищем: перечисление в памяти
Как мы уже договорились выше, у нас будет две реализации взаимодействия с хранилищем: на основе базы данных SQLite и на основе перечисления в памяти. Начнем со второй, так как она проще. Опишем ее в библиотеке классов AspNetCoreStorage.Data.Mock (создадим соответствующий проект).
Нам понадобится реализовать 3 интерфейса из нашего слоя абстракций: IStorageContext, IStorage и IItemRepository (т. к. IItemRepository расширяет IRepository).
Реализация интерфейса IStorageContext в случае с перечислением в памяти не будет содержать никакого кода, это просто пустой класс, поэтому перейдем сразу к IStorage. Класс небольшой, поэтому приведем его здесь целиком:
public class Storage : IStorage
{
public StorageContext StorageContext { get; private set; }
public Storage()
{
this.StorageContext = new StorageContext();
}
public T GetRepository<T>() where T : IRepository
{
foreach (Type type in this.GetType().GetTypeInfo().Assembly.GetTypes())
{
if (typeof(T).GetTypeInfo().IsAssignableFrom(type) && type.GetTypeInfo().IsClass)
{
T repository = (T)Activator.CreateInstance(type);
repository.SetStorageContext(this.StorageContext);
return repository;
}
}
return default(T);
}
public void Save()
{
// Do nothing
}
}
Как видим, класс содержит свойство StorageContext, которое инициализируется в конструкторе. Метод GetRepository перебирает все типы текущей сборки в поисках реализации заданного параметром T интерфейса репозитория. В случае, если подходящий тип обнаружен, создается соответствующий объект репозитория, вызывается его метод SetStorageContext и затем этот объект возвращается. Метод Save не делает ничего. (На самом деле, мы могли бы вообще не использовать StorageContext в этой реализации, передавая null в SetStorageContext, но оставим его для единообразия.)
Теперь посмотрим на реализацию интерфейса IItemRepository:
public class ItemRepository : IItemRepository
{
public readonly IList<Item> items;
public ItemRepository()
{
this.items = new List<Item>();
this.items.Add(new Item() { Id = 1, Name = "Mock item 1" });
this.items.Add(new Item() { Id = 2, Name = "Mock item 2" });
this.items.Add(new Item() { Id = 3, Name = "Mock item 3" });
}
public void SetStorageContext(IStorageContext storageContext)
{
// Do nothing
}
public IEnumerable<Item> All()
{
return this.items.OrderBy(i => i.Name);
}
}
Все очень просто. Метод All возвращает набор элементов из переменной items, которая инициализируется в конструкторе. Метод SetStorageContext не делает ничего, так как никакого контекста в этом случае нам не нужно.
Конкретные реализации взаимодействия с хранилищем: база данных SQLite
Теперь реализуем те же самые интерфейсы, но уже для работы с базой данных SQLite. На этот раз реализация IStorageContext потребует написания некоторого кода:
public class StorageContext : DbContext, IStorageContext
{
private string connectionString;
public StorageContext(string connectionString)
{
this.connectionString = connectionString;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.UseSqlite(this.connectionString);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Item>(etb =>
{
etb.HasKey(e => e.Id);
etb.Property(e => e.Id);
etb.ForSqliteToTable("Items");
}
);
}
}
Как видим, кроме реализации интерфейса IStorageContext этот класс еще и наследует DbContext, представляющий контекст базы данных в Entity Framework Core, чьи методы OnConfiguring и OnModelCreating он и переопределяет (не будем на них останавливаться). Также обратите внимание на переменную connectionString.
Реализация интерфейса IStorage идентична приведенной выше, за исключением того, что в конструктор класса StorageContext необходимо передать строку подключения (конечно, в реальном приложении указывать строку подключения таким образом неправильно, ее следовало бы взять из параметров конфигурации):
this.StorageContext = new StorageContext("Data Source=..\\..\\..\\db.sqlite");
А также, метод Save должен теперь вызывать метод SaveChanges контекста хранилища, унаследованный от DbContext:
public void Save()
{
this.StorageContext.SaveChanges();
}
Реализация интерфейса IItemRepository выглядит теперь таким образом:
public class ItemRepository : IItemRepository
{
private StorageContext storageContext;
private DbSet<Item> dbSet;
public void SetStorageContext(IStorageContext storageContext)
{
this.storageContext = storageContext as StorageContext;
this.dbSet = this.storageContext.Set<Item>();
}
public IEnumerable<Item> All()
{
return this.dbSet.OrderBy(i => i.Name);
}
}
Метод SetStorageContext принимает объект класса, реализующего интерфейс IStorageContext, и приводит его к StorageContext (то есть к конкретной реализации, о которой этот репозиторий осведомлен, так как сам является ее частью), затем с помощью метода Set инициализирует переменную dbSet, которая представляет таблицу в базе данных SQLite. Метод All на этот раз возвращает реальные данные из таблицы базы данных, используя переменную dbSet.
Конечно, если бы у нас было более одного репозитория, было бы логично вынести общую реализацию в какой-нибудь RepositoryBase, где параметр T описывал бы тип модели, параметризировал dbSet и передавался затем в метод Set контекста хранилища.
Взаимодействие веб-приложения с хранилищем
Теперь мы готовы немного модифицировать наше веб-приложение, чтобы заставить его выводить список объектов нашего класса Item на главной странице.
Для начала, добавим ссылки на обе конкретные реализации взаимодействия с хранилищем в раздел dependencies файла project.json основного проекта веб-приложения. В итоге получится как-то так:
"dependencies": {
"AspNetCoreStorage.Data.Mock": "1.0.0",
"AspNetCoreStorage.Data.Sqlite": "1.0.0",
"Microsoft.AspNetCore.Diagnostics": "1.0.0",
"Microsoft.AspNetCore.Mvc": "1.0.1",
"Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
"Microsoft.AspNetCore.Server.Kestrel": "1.0.1",
"Microsoft.Extensions.Logging.Console": "1.0.0",
"Microsoft.NETCore.App": {
"version": "1.0.1",
"type": "platform"
}
}
Теперь перейдем к методу ConfigureServices класса Startup и добавим туда регистрацию сервиса IStorage для двух разных реализаций (одну из них закомментируем, обратите внимание, что реализации регистрируются с помощью метода AddScoped, что означает, что временем жизни объекта является один запрос):
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// Uncomment to use mock storage
services.AddScoped(typeof(IStorage), typeof(AspNetCoreStorage.Data.Mock.Storage));
// Uncomment to use SQLite storage
//services.AddScoped(typeof(IStorage), typeof(AspNetCoreStorage.Data.Sqlite.Storage));
}
Теперь перейдем к контроллеру HomeController:
public class HomeController : Controller
{
private IStorage storage;
public HomeController(IStorage storage)
{
this.storage = storage;
}
public ActionResult Index()
{
return this.View(this.storage.GetRepository<IItemRepository>().All());
}
}
Мы добавили переменную storage типа IStorage и инициализируем ее в конструкторе. Встроенный в ASP.NET Core DI сам передаст зарегистрированную реализацию интерфейса IStorage в конструктор контроллера во время его создания.
Далее, в методе Index мы получаем доступный репозиторий, реализующий интерфейс IItemRepository (напоминаем, все получаемые таким образом репозитории будут иметь единый контекст хранилища благодаря применению шаблона проектирования Единица работы) и передаем в представление набор объектов класса Item, получив их с помощью метода All репозитория.
Теперь выведем полученный список объектов в представлении. Для этого укажем перечисление объектов класса Item в качестве модели вида для представления, а затем в цикле выведем значения свойства Name каждого из объектов:
@model IEnumerable<AspNetCoreStorage.Data.Models.Item>
<h1>Items from the storage:</h1>
<ul>
@foreach (var item in this.Model)
{
<li>@item.Name</li>
}
</ul>
Если сейчас запустить наше веб-приложение мы должны получить следующий результат:
Если же мы поменяем регистрацию реализации интерфейса IStorage на другую, то и результат изменится:
Как видим, все работает!
Заключение
Встроенный в ASP.NET Core механизм внедрения зависимостей (DI) очень упрощает реализацию подобных нашей задач и делает ее более близкой, простой и понятной новичкам. Что касается непосредственно Единицы работы и Репозитория — для типичных веб-приложений это наиболее удачное решение взаимодействия с данными, упрощающее командную разработку и тестирование.
Тестовый проект выложен на GitHub.
Об авторе
Дмитрий Сикорский — владелец и руководитель компании-разработчика программного обеспечения «Юбрейнианс», а также, совладелец киевской службы доставки пиццы «Пиццариум».
Последние статьи по ASP.NET Core
1. Создание внешнего интерфейса веб-службы для приложения.
2. В ногу со временем: Используем JWT в ASP.NET Core.
3. ASP.NET Core на Nano Server.
Are you looking to earn money from your traffic by using popup ads?
ОтветитьУдалитьIn case you are, have you considered using Propeller Ads?