Обычно моя работа связана с разработкой функционала REST веб-сервисов. Чаще всего, разработка эта ведется на базе уже существующих приложений, созданных и настроенных ранее по шаблону Web API в VisualStudio. Создавать новые приложения приходится не часто, последнее созданное мной, было еще на .NET Core 3.1, поэтому, когда возникает подобная задача, приходится тратить время на повторное изучение технологий первоначальной настройки приложения, чтобы оно отвечало всем требованиям бизнес-процесса компании, в которой я работаю. Столкнулся с этой задачей накануне, решил создать шаблон приложения (ссылка на репозиторий GitHub), в котором уже все настроено и готово. Краткое описание процесса привожу в этой статье. Постарался разбить сам процесс на независимые блоки, чтобы для реализации одного из них не приходилось изучать другие. Намеренно подробно освящаю настройку базовых функции, не вдаваясь в описание принципа работы той или иной функции - для более глубокого понимания привожу ссылки на документацию, по которой учился сам. Статья моя будет полезна для новичков в качестве отправной точки для изучения тех или иных функций .Net Core 6, а так же для специалистов, которые как и я, сосредоточены на реализации бизнес-логики приложения и требуется сократить время восстановления в памяти технологии его первоначальной настройки.

Функционал, который требуется настроить

  • Журналирование в файл с репликацией лога

  • Хранение и передачу настроек приложения посредством механизма внедрения зависимостей

  • Настройка CORS политик для доступа к API посредством кросс-доменных запросов

  • Настройка Kestrel на прослушивание определенного порта TCP в режиме публикации селф-хостинг

  • Публикация приложения в качестве службы Windows

Создание сборки

Сборка, в соответствии с документацией, должна включать в себя два приложения:

  • Приложение внешнего слоя - собственно сам Web API с контроллерами и всеми настройками. Это приложение в VisualStudio создается по шаблону "Asp .Net Core Web API" с установкой опций "Use controllers" и "Enable Open API support". Последняя добавляет в проект автодокументацию на swagger - невероятно полезная штука для отладки

  • Приложение уровня ядра для реализации бизнес-логики. Представляет собой библиотеку классов, созданную по шаблону "Class Library"

Настройка работы Kestrel для прослушивания определенного порта в приложении aspnetcore 6

В файле конфигурации appsettings.json

Используется для указания TCP порта встроенному веб-серверу Kestrel при публикации приложения. Добавить параметр Urls:

...
  "Urls": "http://*:5005"
...

В файле конфигурации запуска приложения Properties\launchSettings.json

Используется для запуска Swagger в режиме отладки. Изменить порт в параметре applicationUrl профиля для запуска приложения в режиме селф-хост:

...
"profiles": {
    "WebApiTemplate": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "launchUrl": "swagger",
      "applicationUrl": "http://localhost:5005",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
...

После этих настроек, приложение можно запустить в режиме отладки и убедиться, что swagger доступен на порту 5005 и запрос на созданный по умолчанию метод Get в контроллере WeatherForecast доступен и выполняется на 5005 порту:

Внедрение зависимостей в приложении aspnetcore 6

Документация:

  1. В Program.cs вносим в коллекцию служб приложения требуемый тип. Добавляем строку в любом месте файла между объявлением builder и вызовом его метода сборки Web приложения var app = builder.Build();:

builder.Services.AddTransient<WebApiService>();

Различные варианты жизненных циклов внедряемых зависимостей отлично описаны в документации, приведу только одну, не очевидную, на первый взгляд, ситуацию. Если требуется создать singleton, то такое объявление:

builder.Services.AddSingleton<ISomeClass, SomeClass>();

не приведет к незамедлительному созданию экземпляра класса, он будет создан только тогда, когда произойдет первое создание экземпляра другого класса, который ссылается зависимостью на этот. Для того, чтобы синглтон создавался незамедлительно при старте приложения, объявлять его нужно так:

builder.Services.AddSingleton<ISomeClass>(new SomeClass());

Но тут могут возникнуть трудности, если конструктор класса имеет параметры и тоже требует внедрения зависимостей. Все эти зависимости можно при желании получить из builder.

Передача настроек appsettings.json через внедрение зависимостей в приложении aspnetcore 6

Документация:

  1. Создаем класс для настроек (ApplicationSettings)

  2. Подключаем настройки в Program.cs. Добавляем строку в любом месте файла между объявлением builder и вызовом его метода сборки Web приложения var app = builder.Build();:

builder.Services.Configure<ApplicationSettings>(builder.Configuration);
  1. В классе, где требуется экземпляр объекта настроек, получаем значение через внедрение зависимостей:

  • при помощи NuGet, ставим пакет Microsoft.Extensions.Options

  • объявляем readonly поле:

private readonly ApplicationSettings _settings;
  • внедряем зависимость через конструктор:

public WebApiService(IOptions<ApplicationSettings> options)
{
    _settings = options.Value ?? throw new ArgumentNullException("ApplicationSettings");
}

Настройка NLog в приложении aspnetcore 6

В нашей компании так заведено, что все функции журналирования реализованы с использованием пакета NLog. Эта традиция живет у нас со времен .Net Framework, возможно, в данный момент, в функции журналирования .NET Core уже встроены все преимущества NLog, но мне это не известно, поэтому в данной статье рассматривается именно такой способ.

Документация:

  1. Подключаем NLog в Program.cs. Добавляем строки в любом месте файла между объявлением builder и вызовом его метода сборки Web приложения var app = builder.Build();:

	// NLog: Setup NLog for Dependency injection
    builder.Logging.ClearProviders();
    builder.Host.UseNLog();
  1. В классе, где требуется журналирование, устанавливаем нужные пакеты, внедряем логгер, зависимость, назначив ему соответствующую категорию:

  • при помощи NuGet, если требуется, ставим пакет Microsoft.Extensions.Logging.Abstractions

  • объявляем readonly поле:

private readonly ILogger<WebApiService> _logger; //здесь "WebApiService" - категория журнала, тип, от имени которого будут поступать сообщения
  • внедряем зависимость через конструктор:

public WebApiService(ILogger<WebApiService> logger)
{
    _logger = logger;
}
  1. Запись в журнал:

    _logger.LogDebug($"{logText}, значение параметра из настроек: {_settings.SomeParameter}");
  1. Настраиваем цели для журналирования, категории, репликацию лог файлов, в конфигурации nlog.config. Размещенный в репозитории файл конфигурации уже содержит настройку репликации, при которой хранятся лог-файлы за последние 10 дней. Запись лога в файл настроена так, чтобы не забивало журнал сообщениями используемых библиотек Microsoft.

  2. Проверка. Запускаем приложение, вызываем метод http://localhost:5005/WeatherForecast. Результат работы приложения из приведенного выше репозитория из файла журнала:

2023-01-06 13:10:45.2375 | INFO | Microsoft.AspNetCore.Hosting.Diagnostics | Request starting HTTP/1.1 GET http://localhost:5005/WeatherForecast - - | 
2023-01-06 13:10:45.3266 | DEBUG | WebApiTemplate.Controllers.WeatherForecastController | Проверяем внедрение зависимостей, значение параметра из настроек: Some value | 
2023-01-06 13:10:45.3266 | DEBUG | WebApiTemplate.Core.Services.WebApiService | Проверяем работу сервиса уровня ядра, значение параметра из настроек: Some value | 
2023-01-06 13:10:45.3266 | INFO | WebApiTemplate.Controllers.WeatherForecastController | Action status: Ok | 
2023-01-06 13:10:45.3945 | INFO | Microsoft.AspNetCore.Hosting.Diagnostics | Request finished HTTP/1.1 GET http://localhost:5005/WeatherForecast - - - 200 - application/json;+charset=utf-8 157.2347ms |

В журнале видно поступление запроса на контроллер, начало выполнения метода WebApiTemplate.Controllers.WeatherForecastController.Get, вызов метода сервиса WebApiTemplate.Core.Services.WebApiService.SomeAction, результат работы этого метода и отчет о завершении запроса.

Настройка CORS политик в приложении aspnetcore 6

Документация:

Для моих производственных нужд обычно требуется два набора политик: для отладки (разрешено все) и для публикации (разрешено только то, что требуется)

  1. В appsettings.json создаем свойство AllowedOrigins:

{
  ...
  "AllowedOrigins": [
    "http://localhost:8080"
  ]
}
  1. добавляем параметр в объект настроек

/// <summary>
/// Настройки приложения
/// </summary>
public class ApplicationSettings
{
    ...
    /// <summary>
    /// Разрешенные домены - источники запросов для политики CORS
    /// </summary>
    public string[] AllowedOrigins { get; set; }
}
  1. Так как потребуется получение настроек, в Program.cs создаем экземпляр объекта настроек в любом месте файла между объявлением builder и вызовом его метода сборки Web приложения var app = builder.Build();:

var settings = builder.Configuration.Get<ApplicationSettings>() ?? throw new ArgumentNullException("ApplicationSettings", "Конфигурация не получена");
  1. В Program.cs объявляем CORS политики до вызова builder.Build()

    builder.Services.AddCors(options => options.AddPolicy("AllowAny", builder => builder
        .AllowAnyOrigin()
        .AllowAnyHeader()
        .AllowAnyMethod())
    );

    builder.Services.AddCors(options => options.AddPolicy("AllowSome", builder => builder
        .WithOrigins(settings.AllowedOrigins)
        .AllowAnyHeader()
        .AllowAnyMethod()
        .AllowCredentials())
    );
  1. В зависимости от типа запуска приложения (Debug или Release), подключаем ту или иную политику в файле Program.cs между var app = builder.Build(); и app.Run();:

#if DEBUG
    app.UseCors("AllowAny");
#else
    app.UseCors("AllowSpecURLs");
#endif

Настройка публикации приложения aspnetcore 6 в качестве службы Windows

Если попытаться запустить приложение, как службу, менеджер служб WIndows, ожидая статус запуска из параметров stdout, не получив этот статус, выдаст ошибку "Ошибка 1053 Служба не ответила на запрос своевременно". Для устранения этой ошибки требуется дополнительная настройка приложения.

Документация:

  1. Устанавливаем пакет Microsoft.Extensions.Hosting.WindowsServices

  2. в Program.cs создаем объект настроек и передаем его в конструктор builder взамен строки var builder = WebApplication.CreateBuilder(args);

var options = new WebApplicationOptions
{
    Args = args,
    ContentRootPath = WindowsServiceHelpers.IsWindowsService()
        ? AppContext.BaseDirectory : default
};
var builder = WebApplication.CreateBuilder(options);
  1. Публикуем приложение и размещаем его на сервере Windows любым способом

  2. На сервере любым доступным способом регистрируем и запускаем службу

Комментарии (5)


  1. lair
    06.01.2023 15:40
    +2

    Для того, чтобы синглтон создавался незамедлительно при старте приложения

    А зачем создавать синглтон незамедлительно при старте приложения?

    В зависимости от типа запуска приложения (Debug или Release), подключаем ту или иную политику в файле Program.cs между var app = builder.Build(); и app.Run();:

    А вы про environments в ASP.NET (Core/5+) не слышали?..


    1. ChuckBosack Автор
      07.01.2023 05:26

      А зачем создавать синглтон незамедлительно при старте приложения?

      Чтобы он выполнял свои фоновые задачи независимо от Web Api. Например, при старте приложения он может поднять соединение с внешней системой и обрабатывать ее события.

      А вы про environments в ASP.NET (Core/5+) не слышали?

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


      1. lair
        07.01.2023 05:35
        +4

        Чтобы он выполнял свои фоновые задачи независимо от Web Api.

        Ровно для этого сделаны IHostedService.

        В чем преимущество использования IsDevelopment() в данной ситуации?

        В том, что бинарно идентичный код (или даже контейнер) работает в разработческом и тестовом окружении, а его поведение вы переключаете настройкой (в том числе переменной окружения), без перекомпиляции.


      1. dopusteam
        08.01.2023 17:56
        +2

        Различные варианты жизненных циклов внедряемых зависимостей отлично описаны в документации, приведу только одну, не очевидную, на первый взгляд, ситуацию. Если требуется создать singleton

        Чтобы он выполнял свои фоновые задачи независимо от Web Api. Например, при старте приложения он может поднять соединение с внешней системой и обрабатывать ее события.

        Такие заявления заставляют сомневаться в компетенции.

        А ссылки вида "документация", которые ведут на метанит , ещё усиливают сомнения


  1. s207883
    08.01.2023 17:32
    +2

    Статья на сайте Metanit, спасибо ребятам за толковую документацию

    Люблю тонкий юмор