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

в 10:25, , рубрики: .net, #aspnetcolumn, ASP, ASP.NET, asp.net core, extcore, Блог компании Microsoft

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

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

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

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

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

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

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

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

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 (обратите внимание, что на этот раз это библиотека классов):

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

Чтобы удобно разделять проекты в решении, относящиеся к различным расширениям, переместим наш проект в папку решения с названием 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.

Запуск и тестирование

Наше приложение с двумя расширениями готово. Запустив его, мы должны увидеть нечто подобное:

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

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

Выводы

В настоящий момент мы (я и несколько заинтересовавшихся ребят) активно развиваем этот проект и на нем уже основано несколько других. Будем очень рады идеям, советам и критике. Спасибо!

Ссылка на исходники: https://github.com/ExtCore/ExtCore-Sample.

Авторам

Друзья, если вам интересно поддержать колонку своим собственным материалом, то прошу написать мне на vyunev@microsoft.com для того чтобы обсудить все детали. Мы разыскиваем авторов, которые могут интересно рассказать про ASP.NET и другие темы.

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

Об авторе

Сикорский Дмитрий Александрович
Компания «Юбрейнианс» (http://ubrainians.com/)
Владелец, руководитель
DmitrySikorsky

Автор: Microsoft

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js