Мы продолжаем нашу колонку по теме ASP.NET Core очередной публикацией от Дмитрия Сикорского ( DmitrySikorsky) — руководителя компании «Юбрейнианс» из Украины. В этот раз Дмитрий продолжает рассказ о своем опыте разработки модульного кроссплатформенного фреймворка на базе ASP.NET Core. Предыдущие статьи из колонки всегда можно прочитать по ссылке #aspnetcolumn — Владимир Юнев
В предыдущей статье я уже рассказывал об ExtCore — небольшом фреймворке для разработки модульных и расширяемых приложений на ASP.NET Core. В этой статье я постараюсь более подробно остановится на процессе разработки приложения на его основе.
Основное приложение
Первым делом создадим новый пустой проект на ASP.NET Core 1.0:
В результате мы получим готовый к использованию проект. Осталось только удалить файл Project_Readme.html. Теперь наш обозреватель решений должен выглядеть примерно следующим образом:
Совет! Вы можете попробовать все самостоятельно или загрузив исходный код из GitHub https://github.com/ExtCore/ExtCore-Sample.
Чтобы подключить к нашему проекту фреймворк ExtCore необходимо добавить ссылки на NuGet-пакеты ExtCore.Infrastructure и ExtCore.WebApplication в project.json. Также, т. к. в этом примере мы будем работать с базой данных, добавим туда ссылки и на компоненты расширения ExtCore.Data: (ExtCore.Data, ExtCore.Data.Abstractions, ExtCore.Data.EntityFramework.Sqlite, ExtCore.Data.Models.Abstractions). (Еще нам понадобятся ссылки на привычные для MVC-приложений пакеты, вроде Microsoft.AspNet.Mvc.) В итоге наш project.json должен иметь следующий вид:
{
"commands": {
"web": "Microsoft.AspNet.Server.Kestrel"
},
"dependencies": {
"EntityFramework.Sqlite": "7.0.0-rc1-final",
"ExtCore.Data": "1.0.0-alpha7",
"ExtCore.Data.Abstractions": "1.0.0-alpha7",
"ExtCore.Data.EntityFramework.Sqlite": "1.0.0-alpha7",
"ExtCore.Data.Models.Abstractions": "1.0.0-alpha7",
"ExtCore.Infrastructure": "1.0.0-alpha7",
"ExtCore.WebApplication": "1.0.0-alpha7",
"Microsoft.AspNet.Diagnostics": "1.0.0-rc1-final",
"Microsoft.AspNet.Diagnostics.Entity": "7.0.0-rc1-final",
"Microsoft.AspNet.Mvc": "6.0.0-rc1-final",
"Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-rc1-final",
"Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-final",
"Microsoft.Extensions.Configuration.Abstractions": "1.0.0-rc1-final",
"Microsoft.Extensions.Configuration.Json": "1.0.0-rc1-final",
"Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0-rc1-final"
},
"exclude": [
"wwwroot"
],
"frameworks": {
"dnx451": { },
"dnxcore50": { }
},
"publishExclude": [
"**.user",
"**.vspscc"
],
"version": "1.0.0-*",
"webroot": "wwwroot"
}
Теперь осталось лишь унаследовать класс Startup от ExtCore.WebApplication.Startup:
public class Startup : ExtCore.WebApplication.Startup
{
public Startup(IHostingEnvironment hostingEnvironment, IApplicationEnvironment applicationEnvironment, IAssemblyLoaderContainer assemblyLoaderContainer, IAssemblyLoadContextAccessor assemblyLoadContextAccessor, ILibraryManager libraryManager)
: base(hostingEnvironment, applicationEnvironment, assemblyLoaderContainer, assemblyLoadContextAccessor, libraryManager)
{
IConfigurationBuilder configurationBuilder = new ConfigurationBuilder()
.AddJsonFile("config.json");
this.configurationRoot = configurationBuilder.Build();
}
public override void ConfigureServices(IServiceCollection services)
{
base.ConfigureServices(services);
}
public override void Configure(IApplicationBuilder applicationBuilder, IHostingEnvironment hostingEnvironment)
{
if (hostingEnvironment.IsEnvironment("Development"))
{
applicationBuilder.UseBrowserLink();
applicationBuilder.UseDeveloperExceptionPage();
applicationBuilder.UseDatabaseErrorPage();
}
else
{
applicationBuilder.UseExceptionHandler("/");
}
base.Configure(applicationBuilder, hostingEnvironment);
}
}
В конструкторе класса Startup мы инициализируем переменную configurationRoot, определенную в базовом классе ExtCore.WebApplication.Startup. Это необходимо для предоставления фреймворку ExtCore доступа к параметрам конфигурации (в нашем случае единственным источником параметров конфигурации является файл config.json). Например, расширение ExtCore.Data таким образом получает параметр Data:DefaultConnection:ConnectionString (строку подключения к базе данных). Также можно конфигурировать и другие расширения (в т. ч. свои собственные).
Давайте создадим файл config.json в корне проекта:
{
"Data": {
"DefaultConnection": {
"ConnectionString": "Data Source=../db.sqlite"
}
},
"Extensions": {
"Path": "artifacts\bin\Extensions"
}
}
Параметр Extensions:Path определяет путь, по которому в файловой системе расположена папка с расширениями (относительно корня приложения).
Вот и все, на этом моменте мы можем собрать и запустить наше приложение. Мы получим ошибку 404 и это будет правильно, т. к. у нас пока что нет ни маршрутов, ни контроллеров.
Расширения
Теперь давайте создадим 2 расширения. Первое расширение (ExtensionA) на своей единственной странице (главной странице нашего приложения) будет просто отображать список всех доступных расширений. Также в нем мы протестируем использование статического контента в виде ресурсов на примере CSS-файла. Второе расширение (ExtensionB) будет отображать записи, описанные моделью, из базы данных. Все просто.
Расширение ExtensionA
Создадим еще один проект WebApplication.ExtensionA (обратите внимание, что на этот раз это библиотека классов):
Чтобы удобно разделять проекты в решении, относящиеся к различным расширениям, переместим наш проект в папку решения с названием ExtensionA, предварительно ее создав.
Первым делом опять отредактируем project.json. Добавим ссылку на ExtCore.Infrastructure (содержит описание интерфейса IExtension; кроме того, ExtCore загружает и использует только те сборки, которые имеют ссылку на этот пакет) и на Microsoft.AspNet.Mvc. В этом расширении мы будем использовать представление и CSS-файл, добавленные в виде ресурсов (детальнее я описал это в предыдущей статье, на которую есть ссылка выше), поэтому необходимо также добавить соответствующую запись. Вот что должно получится:
{
"dependencies": {
"ExtCore.Infrastructure": "1.0.0-alpha7",
"Microsoft.AspNet.Mvc": "6.0.0-rc1-final"
},
"frameworks": {
"dnx451": { },
"dnxcore50": { }
},
"resource": [ "Styles/**", "Views/**" ],
"version": "1.0.0-*"
}
Далее, реализуем интерфейс IExtension:
public class ExtensionA : IExtension
{
private IConfigurationRoot configurationRoot;
public string Name
{
get
{
return "Extension A";
}
}
public void SetConfigurationRoot(IConfigurationRoot configurationRoot)
{
this.configurationRoot = configurationRoot;
}
public void ConfigureServices(IServiceCollection services)
{
}
public void Configure(IApplicationBuilder applicationBuilder)
{
}
public void RegisterRoutes(IRouteBuilder routeBuilder)
{
routeBuilder.MapRoute(name: "Extension A", template: "", defaults: new { controller = "ExtensionA", action = "Index" });
}
}
В методе RegisterRoutes мы добавляем маршрут для главной страницы нашего приложения.
Теперь добавим контроллер с единственным методом Index, который будет передавать представлению набор имен всех загруженных ExtCore расширений, для получения которых используется класс ExtensionManager:
public class ExtensionAController : Controller
{
public ActionResult Index()
{
return this.View(ExtensionManager.Extensions.Select(e => e.Name));
}
}
В свою очередь, представление отображает этот набор следующим образом:
<ul>
@foreach (var item in this.Model)
{
<li>@item</li>
}
</ul>
Последнее, что необходимо сделать, это добавить файл стилей typography.css в папку Styles. Выше, в файле project.json, мы указали, что все содержимое папок Styles и Views будет добавлено в сборку в виде ресурсов. ExtCore обнаружит эти ресурсы и сделает возможным их использование аналогичным использованию физических файлов способом. Т. е. мы сможем подключить наш CSS-файл в любом расширении таким образом:
<link href="Styles.typography.css" rel="stylesheet" />
Следует лишь иметь в виду, что древовидная структура файловой системы превращается в «плоскую» структуру текстовых имен (регистр имеет значение!).
Наше расширение ExtensionA готово. Чтобы протестировать его работу достаточно либо добавить ссылку на него в project.json основного приложения, либо собрать его в виде dll-файла и скопировать его в папку с расширениями (мы ранее указали ее в config.json).
Расширение ExtensionB
Здесь нам понадобится целых 4 новых проекта: WebApplication.ExtensionB, WebApplication.ExtensionB.Data.Abstractions, WebApplication.ExtensionB.Data.EntityFramework.Sqlite и WebApplication.ExtensionB.Data.Models. Как и в первом расширении, сгруппируем их в папке решения (с названием ExtensionB).
WebApplication.ExtensionB
В этом проекте мы разместим реализацию интерфейса IExtension, контроллер, модели видов и представления.
Реализация интерфейса IExtension аналогична таковой из предыдущего расширения. Перейдем сразу к контроллеру:
public class ExtensionBController : Controller
{
private IStorage storage;
public ExtensionBController(IStorage storage)
{
this.storage = storage;
}
public ActionResult Index()
{
return this.View(new IndexViewModelBuilder().Build(this.storage.GetRepository<IItemRepository>().All()));
}
}
Т. к. в этом расширении нам необходимо получать некие записи из базы данных, воспользуемся для этого возможностями расширения ExtCore.Data. В конструкторе контроллера запросим у встроенного в ASP.NET DI доступную реализацию интерфейса IStorage (которую раннее обнаружило и зарегистрировало расширение ExtCore.Data). Далее, запросим уже собственную реализацию собственного интерфейса IItemRepository для конкретного хранилища (в нашем случае, это база данных SQLite) и вызовем метод All для получения всех записей. Далее, преобразуем модели из базы данных в модели видов для отображения в представлении.
Вместо использование представлений в виде ресурсов, в этом расширении мы будем использовать предварительно скомпилированные представления. Для этого необходимо добавить класс RazorPreCompilation в папку /Compiler/PreProcess:
public class RazorPreCompilation : RazorPreCompileModule
{
protected override bool EnablePreCompilation(BeforeCompileContext context) => true;
}
Это даст нам возможность использовать собственные (т. е. объявленные внутри нашего расширения) классы для моделей видов. (Более подробно о предварительно скомпилированных представлениях см. в предыдущей статье.)
WebApplication.ExtensionB.Data.Abstractions
Этот проект содержит интерфейс единственного репозитория для работы с моделями типа Item (см. ниже):
public interface IItemRepository : IRepository
{
IEnumerable<Item> All();
}
В нашем примере интерфейс описывает всего лишь один метод для получения всех записей.
WebApplication.ExtensionB.Data.EntityFramework.Sqlite
В этом проекте мы реализуем интерфейс IItemRepository для конкретного хранилища — базы данных SQLite:
public class ItemRepository : RepositoryBase<Item>, IItemRepository
{
public IEnumerable<Item> All()
{
return this.dbSet.OrderBy(i => i.Name);
}
}
Т. к. расширение не работает напрямую с конкретной реализацией, а использует лишь абстракции, мы можем поддерживать одновременно несколько типов хранилищ и добавлять новые без необходимости изменения кода самого расширения.
Также, здесь же происходит и регистрация используемых в расширении моделей и настройки хранилища. Для этого используется класс, реализующий интерфейс IModelRegistrar:
public class ModelRegistrar : IModelRegistrar
{
public void RegisterModels(ModelBuilder modelbuilder)
{
modelbuilder.Entity<Item>(etb =>
{
etb.HasKey(e => e.Id);
etb.Property(e => e.Id);
etb.ForSqliteToTable("Items");
}
);
}
}
WebApplication.ExtensionB.Data.Models
В этом проекте мы описываем нашу единственную модель — Item:
public class Item : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
}
Каждая модель должна реализовать интерфейс ExtCore.Data.Models.Abstractions.IEntity.
Протестируем работу нашего нового расширения точно так, как мы это делали с ExtensionA.
Запуск и тестирование
Наше приложение с двумя расширениями готово. Запустив его, мы должны увидеть нечто подобное:
Выводы
В настоящий момент мы (я и несколько заинтересовавшихся ребят) активно развиваем этот проект и на нем уже основано несколько других. Будем очень рады идеям, советам и критике. Спасибо!
Ссылка на исходники: https://github.com/ExtCore/ExtCore-Sample.
Авторам
Друзья, если вам интересно поддержать колонку своим собственным материалом, то прошу написать мне на vyunev@microsoft.com для того чтобы обсудить все детали. Мы разыскиваем авторов, которые могут интересно рассказать про ASP.NET и другие темы.
Об авторе
Сикорский Дмитрий Александрович
Компания «Юбрейнианс» (http://ubrainians.com/)
Владелец, руководитель
DmitrySikorsky
Автор: Microsoft