Skip to content

Commit 9f9b3db

Browse files
wip improvements
1 parent d2a7126 commit 9f9b3db

7 files changed

Lines changed: 109 additions & 68 deletions

File tree

Directory.Packages.props

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.5" />
4040
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.5" />
4141
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.3" />
42+
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.2.0" />
4243
<PackageVersion Include="Microsoft.SemanticKernel" Version="$(SemanticKernelVersion)" />
4344
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.PgVector" Version="$(SemanticKernelVersion)-preview" />
4445
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.201" />
@@ -51,6 +52,12 @@
5152
<PackageVersion Include="System.CommandLine" Version="2.0.5" />
5253
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
5354
<PackageVersion Include="Octokit" Version="14.0.0" />
55+
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.0" />
56+
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.15.0" />
57+
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.0" />
58+
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.15.0" />
59+
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.0" />
60+
<PackageVersion Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.15.0" />
5461
<PackageVersion Include="DotnetSitemapGenerator" Version="2.0.0" />
5562
</ItemGroup>
5663
</Project>

EssentialCSharp.Chat.Shared/Extensions/ServiceCollectionExtensions.cs

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,7 @@ public static IServiceCollection AddAzureOpenAIServices(
101101

102102
/// <summary>
103103
/// Adds PostgreSQL vector store with managed identity authentication support.
104-
/// NOTE: Token is obtained once at startup and will expire after ~1 hour.
105-
/// For long-running applications, consider implementing token refresh logic.
104+
/// Uses periodic token refresh to ensure tokens are renewed before expiry.
106105
/// </summary>
107106
/// <param name="services">The service collection to add services to</param>
108107
/// <param name="connectionString">The PostgreSQL connection string (without password)</param>
@@ -115,36 +114,39 @@ private static IServiceCollection AddPostgresVectorStoreWithManagedIdentity(
115114
{
116115
credential ??= new DefaultAzureCredential();
117116

118-
// Parse the connection string to extract host, database, and username
119-
var builder = new NpgsqlConnectionStringBuilder(connectionString);
120-
121-
// Check if this is an Azure PostgreSQL connection (contains .postgres.database.azure.com)
122-
bool isAzurePostgres = builder.Host?.Contains(".postgres.database.azure.com", StringComparison.OrdinalIgnoreCase) ?? false;
123-
124-
if (isAzurePostgres && string.IsNullOrEmpty(builder.Password))
125-
{
126-
// Get access token for Azure PostgreSQL using managed identity
127-
var tokenRequestContext = new TokenRequestContext(_PostgresScopes);
128-
var accessToken = credential.GetToken(tokenRequestContext, default);
129-
130-
// Set the password to the access token
131-
builder.Password = accessToken.Token;
132-
133-
// Ensure SSL is enabled for Azure
134-
if (builder.SslMode == SslMode.Disable)
135-
{
136-
builder.SslMode = SslMode.Require;
137-
}
138-
139-
connectionString = builder.ToString();
140-
}
141-
142117
// Register NpgsqlDataSource with UseVector() enabled - this is critical for pgvector support
143118
services.AddSingleton<NpgsqlDataSource>(sp =>
144119
{
120+
var connBuilder = new NpgsqlConnectionStringBuilder(connectionString);
121+
bool isAzurePostgres = connBuilder.Host?.Contains(".postgres.database.azure.com",
122+
StringComparison.OrdinalIgnoreCase) ?? false;
123+
145124
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
146125
// IMPORTANT: UseVector() must be called to enable pgvector support
147126
dataSourceBuilder.UseVector();
127+
128+
if (isAzurePostgres && string.IsNullOrEmpty(connBuilder.Password))
129+
{
130+
// Ensure SSL is enabled for Azure PostgreSQL
131+
if (dataSourceBuilder.ConnectionStringBuilder.SslMode < SslMode.Require)
132+
{
133+
dataSourceBuilder.ConnectionStringBuilder.SslMode = SslMode.Require;
134+
}
135+
136+
// Use periodic token refresh instead of a one-shot token at startup.
137+
// Azure AD tokens expire after ~1 hour; refreshing every 50 minutes
138+
// ensures uninterrupted connectivity for long-running applications.
139+
dataSourceBuilder.UsePeriodicPasswordProvider(
140+
async (_, ct) =>
141+
{
142+
var tokenRequestContext = new TokenRequestContext(_PostgresScopes);
143+
var accessToken = await credential.GetTokenAsync(tokenRequestContext, ct);
144+
return accessToken.Token;
145+
},
146+
TimeSpan.FromMinutes(50),
147+
TimeSpan.FromSeconds(10));
148+
}
149+
148150
return dataSourceBuilder.Build();
149151
});
150152

EssentialCSharp.Web.Tests/WebApplicationFactory.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
5151
services.Remove(dbConnectionDescriptor);
5252
}
5353

54+
// Remove DatabaseMigrationService: it calls MigrateAsync which conflicts
55+
// with EnsureCreated() used below for the in-memory SQLite test database.
56+
ServiceDescriptor? migrationServiceDescriptor = services.SingleOrDefault(
57+
d => d.ImplementationType == typeof(DatabaseMigrationService));
58+
if (migrationServiceDescriptor != null)
59+
{
60+
services.Remove(migrationServiceDescriptor);
61+
}
62+
5463
_Connection = new SqliteConnection(SqlConnectionString);
5564
_Connection.Open();
5665

EssentialCSharp.Web/DatabaseMigrationService.cs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,7 @@ public class DatabaseMigrationService(IServiceScopeFactory services) : Backgroun
1010
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
1111
{
1212
using IServiceScope scope = Services.CreateScope();
13-
EssentialCSharpWebContext? context = scope.ServiceProvider.GetRequiredService<EssentialCSharpWebContext>()
14-
?? throw new InvalidOperationException($"EssentialCSharpWebContext not found for {nameof(DatabaseMigrationService)}");
15-
if (!context.Database.GetPendingMigrations().Contains("20231021170008_CreateIdentitySchema"))
16-
{
17-
await context.Database.MigrateAsync(stoppingToken);
18-
}
19-
else
20-
{
21-
await context.Database.EnsureCreatedAsync(cancellationToken: stoppingToken);
22-
}
13+
EssentialCSharpWebContext context = scope.ServiceProvider.GetRequiredService<EssentialCSharpWebContext>();
14+
await context.Database.MigrateAsync(stoppingToken);
2315
}
2416
}

EssentialCSharp.Web/EssentialCSharp.Web.csproj

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk.Web">
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
22
<PropertyGroup>
33
<TargetFramework>net10.0</TargetFramework>
44
<!--
@@ -27,27 +27,33 @@
2727
</ItemGroup>
2828

2929
<ItemGroup>
30-
<PackageReference Include="Azure.Monitor.OpenTelemetry.AspNetCore" />
3130
<PackageReference Include="AspNet.Security.OAuth.GitHub" />
31+
<PackageReference Include="Azure.Monitor.OpenTelemetry.AspNetCore" />
3232
<PackageReference Include="EssentialCSharp.Shared.Models" />
3333
<PackageReference Include="HtmlAgilityPack" />
3434
<PackageReference Include="IntelliTect.Multitool" />
3535
<PackageReference Include="Mailjet.Api" />
3636
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
3737
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" />
38-
<PackageReference Include="Microsoft.ApplicationInsights.Profiler.AspNetCore" />
3938
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
4039
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" />
4140
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" />
4241
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
4342
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools">
4443
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
4544
</PackageReference>
45+
<PackageReference Include="Microsoft.Extensions.Http.Resilience" />
4646
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" />
4747
<PackageReference Include="Newtonsoft.Json" />
4848
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" />
4949
<PackageReference Include="Octokit" />
5050
<PackageReference Include="DotnetSitemapGenerator" />
51+
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
52+
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
53+
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" />
54+
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
55+
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
56+
<PackageReference Include="OpenTelemetry.Instrumentation.SqlClient" />
5157
</ItemGroup>
5258
<ItemGroup>
5359
<Content Update="wwwroot\images\00mindmap.svg">

EssentialCSharp.Web/Program.cs

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System.Threading.RateLimiting;
2-
using Azure.Monitor.OpenTelemetry.AspNetCore;
32
using EssentialCSharp.Chat.Common.Extensions;
43
using EssentialCSharp.Web.Areas.Identity.Data;
54
using EssentialCSharp.Web.Areas.Identity.Services.PasswordValidators;
@@ -14,7 +13,13 @@
1413
using Microsoft.AspNetCore.Identity;
1514
using Microsoft.AspNetCore.Identity.UI.Services;
1615
using Microsoft.AspNetCore.RateLimiting;
16+
using Azure.Monitor.OpenTelemetry.AspNetCore;
17+
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
1718
using Microsoft.EntityFrameworkCore;
19+
using Microsoft.Extensions.Diagnostics.HealthChecks;
20+
using OpenTelemetry;
21+
using OpenTelemetry.Metrics;
22+
using OpenTelemetry.Trace;
1823

1924
namespace EssentialCSharp.Web;
2025

@@ -24,6 +29,47 @@ private static void Main(string[] args)
2429
{
2530
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
2631

32+
// Health checks (liveness/readiness probes for ACA and standalone hosting)
33+
builder.Services.AddHealthChecks()
34+
.AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
35+
36+
// OpenTelemetry — Aspire injects OTEL_EXPORTER_OTLP_ENDPOINT when hosted.
37+
// Azure Monitor is enabled in production when APPLICATIONINSIGHTS_CONNECTION_STRING is set.
38+
bool useAzureMonitor = !string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]);
39+
builder.Logging.AddOpenTelemetry(logging =>
40+
{
41+
logging.IncludeFormattedMessage = true;
42+
logging.IncludeScopes = true;
43+
});
44+
builder.Services.AddOpenTelemetry()
45+
.WithMetrics(metrics => metrics
46+
.AddAspNetCoreInstrumentation()
47+
.AddHttpClientInstrumentation()
48+
.AddRuntimeInstrumentation())
49+
.WithTracing(tracing =>
50+
{
51+
tracing.AddSource(builder.Environment.ApplicationName);
52+
if (!useAzureMonitor)
53+
{
54+
tracing
55+
.AddAspNetCoreInstrumentation(t =>
56+
t.Filter = ctx =>
57+
!ctx.Request.Path.StartsWithSegments("/health")
58+
&& !ctx.Request.Path.StartsWithSegments("/alive"))
59+
.AddHttpClientInstrumentation()
60+
.AddSqlClientInstrumentation();
61+
}
62+
});
63+
if (!string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]))
64+
builder.Services.AddOpenTelemetry().UseOtlpExporter();
65+
if (useAzureMonitor)
66+
builder.Services.AddOpenTelemetry().UseAzureMonitor();
67+
68+
// HttpClient defaults — standard retry/circuit breaker for all named clients.
69+
builder.Services.ConfigureHttpClientDefaults(http => http.AddStandardResilienceHandler());
70+
71+
72+
2773
builder.Services.Configure<ForwardedHeadersOptions>(options =>
2874
{
2975
options.ForwardedHeaders =
@@ -39,37 +85,12 @@ private static void Main(string[] args)
3985
ConfigurationManager configuration = builder.Configuration;
4086
string connectionString = builder.Configuration.GetConnectionString("EssentialCSharpWebContextConnection") ?? throw new InvalidOperationException("Connection string 'EssentialCSharpWebContextConnection' not found.");
4187

42-
builder.Logging.AddConsole();
43-
builder.Services.AddHealthChecks();
44-
4588
// Create a logger that's accessible throughout the entire method
4689
var loggerFactory = LoggerFactory.Create(loggingBuilder =>
4790
loggingBuilder.AddConsole().SetMinimumLevel(LogLevel.Information));
4891
var initialLogger = loggerFactory.CreateLogger<Program>();
4992

50-
if (!builder.Environment.IsDevelopment())
51-
{
52-
// Configure Azure Application Insights with OpenTelemetry only if connection string is available
53-
var appInsightsConnectionString = builder.Configuration.GetConnectionString("ApplicationInsights")
54-
?? builder.Configuration["ApplicationInsights:ConnectionString"];
55-
56-
if (!string.IsNullOrEmpty(appInsightsConnectionString))
57-
{
58-
builder.Services.AddOpenTelemetry().UseAzureMonitor(
59-
options =>
60-
{
61-
options.ConnectionString = appInsightsConnectionString;
62-
});
63-
builder.Services.AddApplicationInsightsTelemetry();
64-
builder.Services.AddServiceProfiler();
65-
}
66-
else
67-
{
68-
initialLogger.LogWarning("Application Insights connection string not found. Telemetry collection will be disabled.");
69-
}
70-
}
71-
72-
builder.Services.AddDbContext<EssentialCSharpWebContext>(options => options.UseSqlServer(connectionString));
93+
builder.Services.AddDbContext<EssentialCSharpWebContext>(options => options.UseSqlServer(connectionString, sql => sql.EnableRetryOnFailure(5)));
7394
builder.Services.AddDefaultIdentity<EssentialCSharpWebUser>(options =>
7495
{
7596
// Password settings
@@ -289,7 +310,11 @@ await context.HttpContext.Response.WriteAsync(
289310
app.UseForwardedHeaders();
290311
}
291312

292-
app.MapHealthChecks("/healthz");
313+
app.MapHealthChecks("/health");
314+
app.MapHealthChecks("/alive", new HealthCheckOptions
315+
{
316+
Predicate = r => r.Tags.Contains("live")
317+
});
293318

294319
app.UseHttpsRedirection();
295320
app.UseStaticFiles();

docs/getting-started.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ This guide will help you set up your local development environment for working o
55
## What You Will Need
66

77
- [Visual Studio](https://visualstudio.microsoft.com/) (or your preferred IDE)
8-
- [.NET 9.0 SDK](https://dotnet.microsoft.com/download)
8+
- [.NET 10.0 SDK](https://dotnet.microsoft.com/download)
99
- If you already have .NET installed you can check the version by typing `dotnet --info` into cmd to make sure you have the right version
1010

1111
## Startup Steps

0 commit comments

Comments
 (0)