I have a bit of an issue that causes major problem for me at this point, and I can’t seem to find any resolution consulting the docs or in similar questions asked here. I only have this problem when using an EntraApp for authentication.
The problem is that the token doesn’t get renewed once (or after) it expires.
Here’s what I have (that works for exactly 1 hour):
Program.cs
services.AddDbContext<DataverseDatabaseClient>(options =>
{
options.UseSqlServer($"" +
$"Server={Environment.GetEnvironmentVariable("DataverseSettings:SqlServer")};" +
$"Database={Environment.GetEnvironmentVariable("DataverseSettings:SqlDatabase")}");
});
DbContext.cs (with OnModelCreating removed for visual improvement)
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="ModelsD365FinOpsModels**" />
<EmbeddedResource Remove="ModelsD365FinOpsModels**" />
<None Remove="ModelsD365FinOpsModels**" />
</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>
I did experiment with the connection strings found here, and especially with this:
"Authentication=Active Directory Service Principal".
This doesn’t let me sets the .Token property on the SqlConnection() (obviously), and throws an error saying it can’t find my authority, and I can’t find any way to submit my tenantId in those docs either.
So, from my standpoint I have two options, neither of which I know how to proceed to from here:
- Change the authentication process and adding things I might have missed that will make the connection string work, using cliendId/secret.
- Find a way to renew the token using the methods I already have, since I can’t get it done using Program.cs and the dbContext as they look today.
Any help or refactoring would be MUCH appreciated.
Thank you, and feel free to edit the question where applicable.
2
Answers
The answer to my issue might actually be related to this part of Program.cs that flew past right under my nose:
I guess the singleton-part caused my app to reuse an expired instance of the context.
A change to AddScoped seems to have fixed the problem since the token is pulled from cache for each request, unless it haven't expired and will then be refreshed instead.
Short answer: get the token in OnConfiguring.
In the corner case that your DbContext lives longer than the token lifetime, open the SqlConnection before creating the DbContext, and it will remain open for the lifetime of the DbContext.