Как настроить EF Core в функции Azure, изолированной .NET8, с аутентификацией приложения Entra?

У меня есть небольшая проблема, которая на данный момент вызывает у меня серьезные проблемы, и я не могу найти никакого решения, обращаясь к документации или аналогичным вопросам, заданным здесь. У меня эта проблема возникает только при использовании EntraApp для аутентификации.

Проблема в том, что токен не обновляется после (или после) истечения его срока действия.

Вот что у меня есть (работает ровно 1 час):

Программа.cs

        services.AddDbContext<DataverseDatabaseClient>(options =>
    {
        options.UseSqlServer($"" +
            $"Server = {Environment.GetEnvironmentVariable("DataverseSettings:SqlServer")};" +
            $"Database = {Environment.GetEnvironmentVariable("DataverseSettings:SqlDatabase")}");
            
    });

DbContext.cs (с удалением OnModelCreating для визуального улучшения)

using MasterdataAPI.Configurations;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Microsoft.Identity.Client;
using MasterdataAPI.Models.DataverseTableModels;

namespace MasterdataAPI.EFCore
{
    public class DvDbContext : DbContext
    {
        private readonly IMemoryCache _cache;
        private readonly DataverseSettings _dataverseSettings;
        public DbSet<QuoteSqlTableRowModel> Quote { get; set; }

        private string Token => GetToken().Result ?? "";

        public DvDbContext(DbContextOptions<DvDbContext> options, IMemoryCache cache, IOptions<DataverseSettings> dataverseSettings) : base(options)
        {
            _cache = cache;
            _dataverseSettings = dataverseSettings.Value;
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            var connString = $"" +
                $"Server = {_dataverseSettings.SqlServer};" +
                $"Database = {_dataverseSettings.SqlDatabase};";

            var conn = new SqlConnection(connString);
            conn.AccessToken = Token;
            optionsBuilder.UseSqlServer(conn, providerOptions => { providerOptions.EnableRetryOnFailure(); });
        }

        private async Task<string?> GetToken()
        {
            if (!_cache.TryGetValue("dataverseToken", out string? token))
            {
                var tokenResult = await Authenticate();
                token = tokenResult.AccessToken;
                _cache.Set("dataverseToken", tokenResult.AccessToken, tokenResult.ExpiresOn);
            }

            return token;
        }

        private async Task<AuthenticationResult> Authenticate()
        {
            var app = ConfidentialClientApplicationBuilder.Create(_dataverseSettings.ClientId)
            .WithAuthority($"{_dataverseSettings.AuthenticationUrl}/{_dataverseSettings.TenantId}")
            .WithClientSecret(_dataverseSettings.ClientSecret)
            .Build();

            var authResult = await app.AcquireTokenForClient(
                new[] { $"{_dataverseSettings.Audience}/.default" })
                .ExecuteAsync()
                .ConfigureAwait(false);

            return authResult;
        }
    }
}

.csproj

<Project Sdk = "Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <AzureFunctionsVersion>v4</AzureFunctionsVersion>
    <OutputType>Exe</OutputType>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <Compile Remove = "Models\D365FinOpsModels\**" />
    <EmbeddedResource Remove = "Models\D365FinOpsModels\**" />
    <None Remove = "Models\D365FinOpsModels\**" />
  </ItemGroup>
  <ItemGroup>
    <FrameworkReference Include = "Microsoft.AspNetCore.App" />
    <PackageReference Include = "EntityFramework" Version = "6.5.0" />
    <PackageReference Include = "Microsoft.Azure.Functions.Worker" Version = "1.22.0" />
    <PackageReference Include = "Microsoft.Azure.Functions.Worker.Extensions.Http" Version = "3.2.0" />
    <PackageReference Include = "Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version = "1.3.1" />
    <PackageReference Include = "Microsoft.Azure.Functions.Worker.Sdk" Version = "1.17.2" />
    <PackageReference Include = "Microsoft.ApplicationInsights.WorkerService" Version = "2.22.0" />
    <PackageReference Include = "Microsoft.Azure.Functions.Worker.ApplicationInsights" Version = "1.2.0" />
    <PackageReference Include = "Microsoft.Data.SqlClient" Version = "5.2.1" />
    <PackageReference Include = "Microsoft.EntityFrameworkCore" Version = "8.0.6" />
    <PackageReference Include = "Microsoft.EntityFrameworkCore.SqlServer" Version = "8.0.6" />
    <PackageReference Include = "Microsoft.EntityFrameworkCore.Tools" Version = "8.0.6">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include = "Microsoft.Identity.Client" Version = "4.61.3" />
    <PackageReference Include = "Newtonsoft.Json" Version = "13.0.3" />
  </ItemGroup>
  <ItemGroup>
    <None Update = "host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update = "local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
  </ItemGroup>
  <ItemGroup>
    <Using Include = "System.Threading.ExecutionContext" Alias = "ExecutionContext" />
  </ItemGroup>
</Project>

Я экспериментировал со строками подключения, найденными здесь, и особенно с этим: «Аутентификация = Принципал службы Active Directory». Это не позволяет мне установить свойство .Token для SqlConnection() (очевидно) и выдает ошибку, сообщающую, что он не может найти мои полномочия, и я также не могу найти способ отправить свой tenantId в этих документах.

Итак, с моей точки зрения, у меня есть два варианта, ни один из которых я не знаю, как действовать дальше:

  1. Измените процесс аутентификации и добавьте вещи, которые я мог пропустить, чтобы строка подключения работала, используя clientId/secret.
  2. Найдите способ обновить токен, используя уже имеющиеся у меня методы, поскольку я не могу сделать это с помощью Program.cs и dbContext, как они выглядят сегодня.

Любая помощь или рефакторинг будут ОЧЕНЬ оценены.

Спасибо и не стесняйтесь редактировать вопрос, где это применимо.

Пожалуйста, поделитесь своим .csproj файлом.

Harshitha 23.08.2024 14:41

@Харшита Готово!

swaglord mcmuffin' 23.08.2024 14:46
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
2
50
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Краткий ответ: получите токен в OnConfiguring.

В крайнем случае, когда ваш DbContext живет дольше, чем срок действия токена, откройте SqlConnection перед созданием DbContext, и он останется открытым в течение всего времени существования DbContext.

Вы имеете в виду непосредственно в методе inConfiguring(), а не через Token => GetToken в разделе свойств класса? Я попробую это! Спасибо!

swaglord mcmuffin' 26.08.2024 09:45

Да, это особо не помогло. Срок действия соединения по-прежнему истекает с ошибкой «Microsoft.Data.SqlClient.SqlException (0x80131904): Ошибка входа в систему: срок действия соединения истек, повторно подключите окно запроса и повторите попытку».

swaglord mcmuffin' 26.08.2024 10:45
Ответ принят как подходящий

Ответ на мою проблему на самом деле может быть связан с этой частью Program.cs, которая пролетела прямо у меня под носом:

services.AddSingleton<DataverseService>();

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

Изменение в AddScoped, по-видимому, решило проблему, поскольку токен извлекается из кеша для каждого запроса, если только срок его действия не истек, и вместо этого он будет обновлен.

services.AddScoped<DataverseService>();    

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