Внедрение ITokenAcquisition в службу Singleton не работает с ASPNETCORE_ENVIRONMENT=Development

Я заметил кое-что странное при внедрении ITokenAcquisition.
Вы можете найти мой код для воспроизведения проблемы здесь: Github Repo
Я использую NuGet Microsoft.Identity.Web 2.18.0 для включения аутентификации MSAL в моем ASP.NET Api (.NET 7).

Мой Program.cs выглядит так:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration)
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddInMemoryTokenCaches();

// Add services to the container.

builder.Services.AddControllers();
builder.Services.AddHostedService<QueuedHostedService>();
var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Вы можете видеть, что вместе с моим API также работает фоновая служба:

public class QueuedHostedService : BackgroundService
{
    ITokenAcquisition _tokenAcquisition;
    public QueuedHostedService(ITokenAcquisition tokenAcquisition)
    {
        _tokenAcquisition = tokenAcquisition;
    }


    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await BackgroundProcessing(stoppingToken);
    }

    private async Task BackgroundProcessing(CancellationToken stoppingToken)
    {
        var token = await _tokenAcquisition.GetAccessTokenForAppAsync("api://xxxxx-5a7f-430e-8ea1-6e133055990e/.default");
        while (!stoppingToken.IsCancellationRequested)
        {
            Console.WriteLine("Executing...");
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        // "Queued Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}

Эта служба использует ITokenAcquisition для получения токена доступа для другого API (это может быть граф или что-то в этом роде).

Действие моего контроллера выглядит почти стандартно, но имеет атрибут Authorize

[Authorize]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    ITokenAcquisition _tokenAcquisition;
    public WeatherForecastController(ITokenAcquisition tokenAcquisition) 
    { 
        _tokenAcquisition = tokenAcquisition;
    }

    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    [HttpGet]
    public async Task<IEnumerable<WeatherForecast>> GetAsync()
    {
        var token = await _tokenAcquisition.GetAccessTokenForAppAsync("api://xxxxxx-5a7f-430e-8ea1-6e133055990e/.default");
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

Мой код работает, и фоновая служба получает токен доступа, но только тогда, когда для переменной ASPNETCORE_ENVIRONMENT установлено значение «Тест» или «Производство».

В разделе «Разработка» моя служба завершается с ошибкой со следующим сообщением об ошибке:

Невозможно использовать службу с ограниченной областью действия «Microsoft.Identity.Web.ITokenAcquisition» из синглтона «Microsoft.Extensions.Hosting.IHostedService».)'

Кто-нибудь знает:

  • Разрешено ли внедрение ITokenAcquisition в фоновую службу?
  • Как может быть, что он работает в режимах тестирования или производства, но никогда не работает в режиме разработки?
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
131
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Измените свой QueuedHostedService.cs, как показано ниже, проблема может быть решена.

using Microsoft.Identity.Web;
using static System.Formats.Asn1.AsnWriter;

namespace DataHive.Enterprises.Core.Services
{
    public class QueuedHostedService : BackgroundService
    {
        //ITokenAcquisition _tokenAcquisition;
        private readonly IServiceScopeFactory _scopeFactory;
        public QueuedHostedService(IServiceScopeFactory scopeFactory)
        {
            _scopeFactory = scopeFactory;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            await BackgroundProcessing(stoppingToken);
        }

        private async Task BackgroundProcessing(CancellationToken stoppingToken)
        {
            using (var scope = _scopeFactory.CreateScope())
            {
                var tokenAcquisition = scope.ServiceProvider.GetRequiredService<ITokenAcquisition>();
                var token = await tokenAcquisition.GetAccessTokenForAppAsync("api://0f5ff0f2-5a7f-430e-8ea1-6e133055990e/.default");

                while (!stoppingToken.IsCancellationRequested)
                {
                    Console.WriteLine("Executing...");
                }
            }
        }

        public override async Task StopAsync(CancellationToken stoppingToken)
        {
            // "Queued Hosted Service is stopping.");

            await base.StopAsync(stoppingToken);
        }
    }
}

Кроме того, в цикле while (!stoppingToken.IsCancellationRequested) вы можете либо действительно сделать что-то полезное, либо немного поспать (Task.Delay).

Ricardo Peres 30.04.2024 15:39

Почему? Вредно ли просто запустить цикл? Можете ли вы рассказать мне больше?

David Mason 02.05.2024 22:47
Ответ принят как подходящий

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

Это суть вашей проблемы. При работе в «режиме разработки» (когда для этой переменной среды установлено значение Development) контейнер Microsoft.Extensions.DependencyInjection будет выполнять то, что они называют проверкой области действия, то есть он попытается увидеть, не нарушает ли конфигурация вашего контейнера изоляцию области: если время жизни зависимость короче времени жизни содержащего ее класса.

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

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

Разрешено ли внедрение ITokenAcquisition в фоновую службу?

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

Если у вас есть фоновая служба, выполняющая множество задач, вам необходимо ограничить разрешение зависимостей этими задачами, а не самой фоновой службой.

Обычно рекомендуется отделить размещенную службу от процессора задач. Затем вы можете сделать что-то вроде этого:

public class QueuedHostedService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    
    public QueuedHostedService(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await BackgroundProcessing(stoppingToken);
    }

    private async Task BackgroundProcessing(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            // Get item from your queue...
            var queueItem = ...
            
            await using var scope = _scopeFactory.CreateAsyncScope();
            var processor = scope.ServiceProvider.GetRequiredService<IQueueItemProcessor>();
            
            await processor.ProcessAsync(queueItem); 
        }
    }
}

Затем вы создаете отдельный класс, который выполняет обработку элемента:

public class QueueItemProcessor : IQueueItemProcessor
{
    private readonly ITokenAcquisition _tokenAcquisition;

    public QueueItemProcessor(ITokenAcquisition tokenAcquisition)
    {
        _tokenAcquisition = tokenAcquisition;
    }

    public async Task ProcessAsync(QueueItem queueItem)
    {
        Console.WriteLine("Executing...");
        var token = await _tokenAcquisition.GetAccessTokenForAppAsync("api://xxxxx-5a7f-430e-8ea1-6e133055990e/.default");
        
        // perform call with token here...
    }
}

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

Спасибо! Это уже многое прояснило. Но два вопроса: что вообще означает «область действия» в фоновой службе? Нет никакого «запроса», как в API или около того. Так что же такое «размах»?

David Mason 02.05.2024 22:51

И: как я могу узнать, является ли служба (т. е. ITokenAquisition) ограниченной, временной или одноэлементной?

David Mason 02.05.2024 22:53

@DavidMason понятие «объем» произвольно. Он используется для запросов, но это не единственный вариант его использования. Вы можете объявить новую область на любой границе, где хотите изолировать разрешенные экземпляры от контейнера. В веб-приложении он используется для запросов. Если у вас есть средство выполнения фоновых заданий, вы можете ограничить выполнение каждого задания. Так далее и так далее. В далеком прошлом контейнеры имели «жестко запрограммированную» поддержку времени жизни «для каждого запроса», но с тех пор это было обобщено до «ограниченной области», которая имеет более широкое применение, но по-прежнему работает так же, как и для каждого запроса на веб-уровне.

julealgon 03.05.2024 15:09

@DavidMason вы можете проверить время жизни чего-то, добавленного библиотекой, либо заглянув в ее документацию и посмотрев, указано ли они там это (в идеале, они должны это делать), либо вы можете просто проверить либо IServiceCollection, либо встроенный IServiceProvider во время отладки и проверить срок действия регистрации вручную. Насколько я знаю, более простого способа не существует. Некоторые реализации сторонних контейнеров имеют своего рода «представление отладки» для своих контейнеров, поэтому вы также можете искать их, если используете сторонний контейнер. Хотя я бы, наверное, просто проверил IServiceCollection.

julealgon 03.05.2024 15:15

@DavidMason обратите внимание, что IServiceCollection по сути является псевдонимом ICollection<ServiceDescriptor>, где ServiceDescriptor — это модель, содержащая информацию о регистрации. Одно из его свойств — Lifetime, которое должно сказать вам, является ли это временным, ограниченным или одноэлементным экземпляром.

julealgon 03.05.2024 15:16

Другие вопросы по теме