Готовим ASP.NET Core: подробнее про работу с модульным фреймворком

от автора


Мы продолжаем нашу колонку по теме ASP.NET Core очередной публикацией от Дмитрия Сикорского ( DmitrySikorsky) — руководителя компании «Юбрейнианс» из Украины. В этот раз Дмитрий продолжает рассказ о своем опыте разработки модульного кроссплатформенного фреймворка на базе ASP.NET Core. Предыдущие статьи из колонки всегда можно прочитать по ссылке #aspnetcolumn — Владимир Юнев

В предыдущей статье я уже рассказывал об ExtCore — небольшом фреймворке для разработки модульных и расширяемых приложений на ASP.NET Core. В этой статье я постараюсь более подробно остановится на процессе разработки приложения на его основе.

Основное приложение

Первым делом создадим новый пустой проект на ASP.NET Core 1.0:

В результате мы получим готовый к использованию проект. Осталось только удалить файл Project_Readme.html. Теперь наш обозреватель решений должен выглядеть примерно следующим образом:

aspnetcolumngithubСовет! Вы можете попробовать все самостоятельно или загрузив исходный код из 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

ссылка на оригинал статьи https://habrahabr.ru/post/279985/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *