Я заметил кое-что странное при внедрении 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».)'
Кто-нибудь знает:
Измените свой 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);
}
}
}
Почему? Вредно ли просто запустить цикл? Можете ли вы рассказать мне больше?
Как может быть, что он работает в режимах тестирования или производства, но никогда не работает в режиме разработки?
Это суть вашей проблемы. При работе в «режиме разработки» (когда для этой переменной среды установлено значение 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 или около того. Так что же такое «размах»?
И: как я могу узнать, является ли служба (т. е. ITokenAquisition) ограниченной, временной или одноэлементной?
@DavidMason понятие «объем» произвольно. Он используется для запросов, но это не единственный вариант его использования. Вы можете объявить новую область на любой границе, где хотите изолировать разрешенные экземпляры от контейнера. В веб-приложении он используется для запросов. Если у вас есть средство выполнения фоновых заданий, вы можете ограничить выполнение каждого задания. Так далее и так далее. В далеком прошлом контейнеры имели «жестко запрограммированную» поддержку времени жизни «для каждого запроса», но с тех пор это было обобщено до «ограниченной области», которая имеет более широкое применение, но по-прежнему работает так же, как и для каждого запроса на веб-уровне.
@DavidMason вы можете проверить время жизни чего-то, добавленного библиотекой, либо заглянув в ее документацию и посмотрев, указано ли они там это (в идеале, они должны это делать), либо вы можете просто проверить либо IServiceCollection
, либо встроенный IServiceProvider
во время отладки и проверить срок действия регистрации вручную. Насколько я знаю, более простого способа не существует. Некоторые реализации сторонних контейнеров имеют своего рода «представление отладки» для своих контейнеров, поэтому вы также можете искать их, если используете сторонний контейнер. Хотя я бы, наверное, просто проверил IServiceCollection
.
@DavidMason обратите внимание, что IServiceCollection
по сути является псевдонимом ICollection<ServiceDescriptor>
, где ServiceDescriptor — это модель, содержащая информацию о регистрации. Одно из его свойств — Lifetime
, которое должно сказать вам, является ли это временным, ограниченным или одноэлементным экземпляром.
Кроме того, в цикле while (!stoppingToken.IsCancellationRequested) вы можете либо действительно сделать что-то полезное, либо немного поспать (Task.Delay).