Andrey on .NET | ASP.NET Core: Конфигурация приложения

ASP.NET Core: Конфигурация приложения

ASP.NET logoИзменения в ASP.NET Core, по сравнению с обычным ASP.NET, затронули различные части платформы. Одной из них является конфигурация и запуск веб-приложения. Давайте посмотрим как это происходит и что доступно для настройки под конкретное приложение.

Создадим пустое ASP.NET Core веб-приложение. Оно будет выглядеть следующим образом:

Empty WebApp structure

Здесь всего 2 файла с C# кодом: Program.cs и Startup.cs. Имя первого может навести на мысли о консольном приложении. И это действительно так. Веб-приложение в  ASP.NET Core является, по сути, консольным. Это легко проверить посмотрев свойства проекта. Соответственно выполнение кода начнется с Program.cs

Program.cs – конфигурация окружения (environment)

Посмотрим на код Program.cs:

using System.IO;
using Microsoft.AspNetCore.Hosting;

namespace WebApplicationStartupDemo
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = new WebHostBuilder()
                   .UseKestrel()
                   .UseContentRoot(Directory.GetCurrentDirectory())
                   .UseIISIntegration()
                   .UseStartup<Startup>()
                   .Build();

            host.Run();
        }
    }
}

Метод Main(…) предназначен для добавления сервисов, которые, как правило, связанны со средой выполнения веб-приложения. В данном случае указывается использование сервера Kestrel, который интегрирован с IIS и задается место расположения файлов веб-приложения.

Экземпляр класса WebHostBuilder реализует интерфейс IWebHostBuilder:

public interface IWebHostBuilder
{
    IWebHost Build();
    IWebHostBuilder ConfigureLogging(Action<ILoggerFactory> configureLogging);
    IWebHostBuilder ConfigureServices(Action<ServiceCollection> configureServices);
    string GetSetting(string key);
    IWebHostBuilder UseLoggerFactory(ILoggerFactory loggerFactory);
    IWebHostBuilder UseSetting(string key, string value);
}

Здесь стоит отметить метод ConfigureServices(…), который позволяет добавлять собственные сервисы в контейнер внедрения зависимостей (dependency injection). При этом для добавления группы связанных сервисов лучше создавать методы-расширения UseNNN(…), т.к. такой однотипный код более удобный для чтения.

public static IWebHostBuilder UseMyServices(this IWebHostBuilder webHostBuilder)
{
    webHostBuilder.ConfigureServices((Action) (collection =>
    {
        collection.Add(new ServiceDescriptor(typeof(MyServiceInterface1), typeof(MyService1), ServiceLifetime.Transient));
        ...
        collection.Add(new ServiceDescriptor(typeof(MyServiceInterfaceN), typeof(MyServiceN), ServiceLifetime.Singleton));
    }));

    return webHostBuilder;
}

Параметр collection имеет тип IServiceCollection. Рассмотрим его чуть позже. А сейчас необходимо отметить, что Program.Main(…) рекомендуется использовать только для настройки среды выполнения и добавлением связанных с ней сервисов в контейнер. Для конфигурации самого веб-приложения предусмотрен отдельный класс. Обратите внимание на выделенную в коде Program.cs строку с вызовом UseStartup<Startup>(…). ��менно она указывает где расположена конфигурация веб-приложения.

Startup.cs – конфигурация веб-приложения

Класс конфигурации Startup, расположенный по умолчанию в файле Startup.cs, может содержать конструктор и 2 специальных метода. При этом только метод Configure(…) является обязательным.  Все они вызываются ASP.NET Core в процессе запуска веб-приложения:

public class Startup
{
    public Startup(IHostingEnvironment env)
    {
    }

    public void ConfigureServices(IServiceCollection services)
    {
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
    }
}

Конструктор

Конструктор не является обязательным. В качестве его параметра можно указать интерфейс IHostingEnvironment, представляющий абстракцию хостинга веб-приложения.

Вместе с тем, инициализация и настройка веб-приложения в конструкторе объекта не является хорошей практикой. Такой подход стоит использовать только при необходимости.

ConfigureServices – настраиваем контейнер с сервисами

Первый из методов класса Startup также является опциональным. Он отвечает за конфигурацию контейнера для внедрения зависимостей. Его единственный параметр IServiceCollection services является наследником списком IList<ServiceDescriptor>.

public interface IServiceCollection : IList<ServiceDescriptor>,
    ICollection<ServiceDescriptor>,
    IEnumerable<ServiceDescriptor>,
    IEnumerable {  }

ServiceDescriptor описывает пару, состоящую из интерфейса и типа его реализации для регистрации в контейнере. Вместо последнего может быть указан фабричный метод или уже созданный объект.  Кроме того, ServiceDescriptor содержит указания на время жизни создаваемого объекта:

  • Singleton – всегда будет использоваться единственный экземпляр объекта.
  • Scoped – свой экземпляр будет создаваться для каждой определенной области видимости объекта (scope).
  • Transient – каждый запрос к контейнеру будет возвращать новый результат.

В ServiceDescriptor добавлен ряд статических методов, упрощающих создание его экземпляров:

  • ServiceDescriptor.Describe(…) – позволяет задать интерфейс, тип реализации или фабрику, а также время жизни объекта.
  • Группа методов ServiceDescriptor.Singleton / ServiceDescriptor.Scoped / ServiceDescriptor.Transient – создает объект с указанным в имени метода временем жизни.

Также для удобства, в ASP.NET Core определены методы-расширения для IServiceCollection, такие как:

  • Add(…) – перегруженный метод, который позволяет добавить сразу несколько описаний.
  • TryAdd(…) – добавляет одно или несколько описаний, только если их интерфейсы еще не были добавлены.
  • Replace(…) – заменяет описание с таким же интерфейсом.
  • Группа методов TryAddSingleton / TryAddScoped / TryAddTransient позволяющие добавлять описания с заданным временем жизни создаваемого объекта. При этом время жизни заданное в самом ServiceDescriptor игнорируется.

Стоит отметить, что многие библиотеки предоставляют методы вида AddNNN(…), которые заполняют контейнер необходимыми типами. Например, для использования ASP.NET MVC необходимо вызывать services.AddMvc(…).

Пример конфигурации контейнера:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddMemoryCache();
    services.Add(new ServiceDescriptor(typeof(IUserService), typeof(UserService), ServiceLifetime.Singleton));
    services.Add(new ServiceDescriptor(typeof(IBlogService), typeof(BlogService), ServiceLifetime.Singleton));
}
Если в проекте используются сборки, например BusnessLayer и DataAccessLayer, то в них можно создать методы расширения UseBusinessLayer(this IServiceCollection services) и UseDataAccessLayer(this IServiceCollection services) для заполнения списка зависимостей и вызывать их из ConfigureServices(…). Это сделает конфигурацию более понятной при чтении.

Configure – настраиваем конвейер приложения

Второй метод, Configure(…), является обязательным. При его отсутствии ASP.NET Core выкинет исключение InvalidOperationException. Именно этот метод отвечает за добавление модулей middleware, тем самым определяя конвейер обработки запросов веб-приложения.

В ASP.NET Core веб-приложении изначально конвейер обработчиков пустой. Поэтому добавление хотя бы одного модуля является необходимым действием.

В параметрах этого метода передаются:

  • IApplicationBuilder – предоставляет доступ к текущей среде выполнения:
    • Use(…) – добавляет модули в конвейер обработки запросов.
    • IServiceProvider ApplicationServices – предоставляет доступ к контейнеру зависимостей.
    • IFeatureCollection ServerFeatures – описывает возможности сервера.
    • IDictionary<string, object> Properties – является словарем, доступным для всех модулей в процессе регистрации.
  • IHostingEnvironment – предоставляет сведения о хостинге веб-приложения и содержит следующие свойства:
    • WebRootPath – абсолютный путь до корневой директории веб-приложения.
    • ContentRootPath – абсолютный путь до директории с контентом веб-приложения.
    • WebRootFileProvider / ContentRootFileProvider – интерфейсы IFileProvider для получения информации о файлах в указанных директориях. Они также могут создавать Stream для чтения самих файлов.
  • ILoggerFactory – позволяет добавлять провайдеры для записи диагностических сообщений. Состоит из двух методов:
    • AddProvider(…) – добавляет новый провайдер сохранения диагностических сообщений.
    • CreateLogger(…) – создает экземпляр интерфейса ILogger для записи диагностических сообщений в журнал.

Аналогично конфигурации контейнера зависимостей, многие библиотеки предоставляют методы UseNNN(…) для регистрации необходимых для них модулей. Например в ASP.NET MVC это UseMvc(…), в который передаются уже настройки самого MVC.

В ASP.NET Core важно в каком порядке, при помощи метода Use(…) или его аналогов, были добавлены модули в конвейер. Именно в таком порядке будет они будут обрабатывать запрос.

Пример конфигурации:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    var builder = new ConfigurationBuilder()
        .SetBasePath(env.ContentRootPath)
        .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
        .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
        .AddEnvironmentVariables();

    var configuration = builder.Build();

    loggerFactory.AddConsole(сonfiguration.GetSection("Logging"));
    loggerFactory.AddDebug();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseBrowserLink();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseStaticFiles();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}");
    });
}

Класс конфигурации не обязательно должен называться Startup. Можно использовать любое имя для класса и файла, которое необходимо указать при вызове UseStartup в Program.cs.

Способы регистрации модулей

В ASP.NET Core существует ряд методов-расширений IApplicationBuilder, предназначенных для более удобной регистрации модулей:

  • Run(RequestDelegate handler) – добавляет последний модуль в конвейер. Его отличие от Use() заключается в отсутствии делегата "следующего модуля". Пример:
    app.Run(async (HttpContext context) => {
        await context.Response.WriteAsync("Hello World!");
    });
    При этом любые модули, добавленные вызовами Use(...) после, не получает управления ни при каких условиях.
  • UseMiddleware<TMiddleware>(params object[] args) и UseMiddleware(Type midllewareType, params object[] args) - позволяют зарегистрировать модули middleware, созданные в виде классов.
  • Map(…) – позволят добавить модуль, который будет вызываться только для запроса к адресу, начинающемуся с заданного текста. Пример: любой запрос начинающийся с "/custom" должен выводить текст "Hello Custom World!".
    app.Map("/custom", (IApplicationBuilder builer) => {
        builer.Run(async (context) => {
            await context.Response.WriteAsync("Hello Custom World!");
        });
    });
  • MapWhen(…) – добавляет модуль, который будет вызван только если заданное условие истинно. Пример: вывести текст "This is GET" для всех GET запросов.
    app.MapWhen((HttpContext context) => context.Request.Method == "GET",
        (IApplicationBuilder builer) => {
            builer.Run(async (context) => {
                await context.Response.WriteAsync("This is GET");
            });
        });
    

Использование различных конфигураций для разных сред

ASP.NET Core предоставляет несколько вариантов использования различных конфигураций.

Можно самостоятельно определять класс, который будет указан в UseStartup. Плюсом такого решения являет возможность реализации любой логики выбора необходимой конфигурации.

В данном случае будет удобнее использовать вариант с указанием типа в качестве параметра:
UseStartup(Type startupClassType).

В ASP.NET Core существует собственный вариант. Он основан на использовании значения переменной среды ASPNETCORE_ENVIRONMENT в именах методов Configure(…) и ConfigureServices(…):

  • Configure{ASPNETCORE_ENVIRONMENT}(…)
  • Configure{ASPNETCORE_ENVIRONMENT}Services(…)

Например, можно создать ConfigureDevelopment(…) и ConfigureProduction(…). Соответственно первый будет вызываться в development среде, а второй в production.

Кроме того, можно исключить обращение к UseStartup(…) из Program.Main(…). Тогда ASP.NET Core будет использовать имя по умолчанию, Startup, и искать следующие типы (в порядке приоритета):

  • Startup{ASPNETCORE_ENVIRONMENT}, если определена переменная среды ASPNETCORE_ENVIRONMENT.
  • Startup

Подходы к выбору конфигурации можно комбинировать. Например, самостоятельно выбирать класс конфигурации исходя из типа текущей серверной ОС. Но в каждом из них определить свои методы для development и production сред.

Комментарии (3) -

Василий 20.03.2018 9:03:15

Андрей, спасибо за хороший сайт. Всё интересно и по делу.
    Я по поводу слов так и же: видно, у вас где-то в шаблоне опечатка - также пишем вместе, если можем заменить на тоже; так же пишем раздельно, если можем заменить на как и в случае N.

С уважением,
Головко В.

Алексей 01.10.2019 3:50:32

Здравствуйте. Нашёл опечатки, пока читал:
IFeatureCollection ServerFeatures – описывает Http возможностЕЙ сервера.
ILoggerFactory – позволяет добавлять провайдеры для записи диагностических сообщений. Состоит ИХ двух методов:
MapWhen(…) – добавляет модуль, который будет вызываЕТСЯ только если заданное условие истинно.

Алексей Спасибо.

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