Skip to content

Commit 901ec47

Browse files
authored
feat: Add .NET Aspire (#2827)
* feat: Add .NET Aspire * fix: Fix the Connection String * fix: Update markdown and published * fix: publish the API only
1 parent 7c23544 commit 901ec47

21 files changed

+427
-31
lines changed

.github/workflows/build-and-dockerize.yml

-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ jobs:
3939
uses: actions/setup-dotnet@v4
4040
with:
4141
dotnet-version: '9.0.x'
42-
dotnet-quality: 'preview'
4342
- name: Install dependencies
4443
run: dotnet restore
4544
- name: Build
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<Sdk Name="Aspire.AppHost.Sdk" Version="9.1.0" />
3+
4+
<PropertyGroup>
5+
<OutputType>Exe</OutputType>
6+
<TargetFramework>net9.0</TargetFramework>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
<Nullable>enable</Nullable>
9+
<IsAspireHost>true</IsAspireHost>
10+
<UserSecretsId>0f1966e4-7b2b-4a28-a9b6-8198aab433ec</UserSecretsId>
11+
</PropertyGroup>
12+
13+
<ItemGroup>
14+
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.1.0" />
15+
<PackageReference Include="Aspire.Hosting.Azure.ServiceBus" Version="9.1.0" />
16+
<PackageReference Include="Aspire.Hosting.Azure.Storage" Version="9.1.0" />
17+
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="9.1.0" />
18+
<PackageReference Include="Aspire.Hosting.Redis" Version="9.1.0" />
19+
</ItemGroup>
20+
21+
<ItemGroup>
22+
<ProjectReference Include="..\BervProject.WebApi.Boilerplate\BervProject.WebApi.Boilerplate.csproj" />
23+
<ProjectReference Include="..\BervProject.WebApi.Boilerplate.MigrationService\BervProject.WebApi.Boilerplate.MigrationService.csproj" />
24+
</ItemGroup>
25+
26+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
var builder = DistributedApplication.CreateBuilder(args);
2+
3+
var cache = builder.AddRedis("cache").WithRedisInsight();
4+
var postgres = builder.AddPostgres("postgres").WithPgAdmin();
5+
var postgresdb = postgres.AddDatabase("postgresdb");
6+
var serviceBus = builder.AddAzureServiceBus("messaging").RunAsEmulator();
7+
var storage = builder.AddAzureStorage("storage").RunAsEmulator();
8+
var blobs = storage.AddBlobs("blobs");
9+
var queues = storage.AddQueues("queues");
10+
var tables = storage.AddTables("tables");
11+
12+
var migration = builder.AddProject<Projects.BervProject_WebApi_Boilerplate_MigrationService>("migrations")
13+
.WithReference(postgresdb, connectionName: "BoilerplateConnectionString")
14+
.WithExplicitStart();
15+
16+
builder.AddProject<Projects.BervProject_WebApi_Boilerplate>("apiservice")
17+
.WithHttpEndpoint()
18+
.WithReference(cache, connectionName: "Redis")
19+
.WithReference(postgresdb, connectionName: "BoilerplateConnectionString")
20+
.WithReference(blobs, connectionName: "AzureStorageBlob")
21+
.WithReference(queues, connectionName: "AzureStorageQueue")
22+
.WithReference(tables, connectionName: "AzureStorageTable")
23+
.WithReference(serviceBus, connectionName: "AzureServiceBus")
24+
.WaitFor(cache)
25+
.WaitFor(postgresdb)
26+
.WaitFor(blobs)
27+
.WaitFor(queues)
28+
.WaitFor(tables)
29+
.WaitFor(serviceBus)
30+
.WaitForCompletion(migration);
31+
32+
builder.Build().Run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning"
6+
}
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning",
6+
"Aspire.Hosting.Dcp": "Warning"
7+
}
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Worker">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<UserSecretsId>dotnet-BervProject.WebApi.Boilerplate.MigrationService-02d1add9-9cd8-4b0e-96ab-9dedc82c871c</UserSecretsId>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.1.0" />
12+
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.3" />
13+
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.3" />
14+
</ItemGroup>
15+
16+
<ItemGroup>
17+
<ProjectReference Include="..\BervProject.WebApi.Boilerplate\BervProject.WebApi.Boilerplate.csproj" />
18+
<ProjectReference Include="..\BervProject.WebApi.Boilerplate.ServiceDefaults\BervProject.WebApi.Boilerplate.ServiceDefaults.csproj" />
19+
</ItemGroup>
20+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using BervProject.WebApi.Boilerplate.EntityFramework;
2+
using BervProject.WebApi.Boilerplate.MigrationService;
3+
4+
var builder = Host.CreateApplicationBuilder(args);
5+
6+
builder.AddServiceDefaults();
7+
8+
builder.Services.AddHostedService<Worker>();
9+
10+
builder.Services.AddOpenTelemetry()
11+
.WithTracing(tracing => tracing.AddSource(Worker.ActivitySourceName));
12+
builder.AddNpgsqlDbContext<BoilerplateDbContext>("BoilerplateConnectionString");
13+
14+
var host = builder.Build();
15+
host.Run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System.Diagnostics;
2+
using BervProject.WebApi.Boilerplate.EntityFramework;
3+
using Microsoft.EntityFrameworkCore;
4+
5+
namespace BervProject.WebApi.Boilerplate.MigrationService;
6+
7+
public class Worker : BackgroundService
8+
{
9+
public const string ActivitySourceName = "Migrations";
10+
private static readonly ActivitySource SActivitySource = new(ActivitySourceName);
11+
12+
private readonly IServiceProvider _serviceProvider;
13+
private readonly IHostApplicationLifetime _hostApplicationLifetime;
14+
15+
public Worker(IServiceProvider serviceProvider,
16+
IHostApplicationLifetime hostApplicationLifetime)
17+
{
18+
_serviceProvider = serviceProvider;
19+
_hostApplicationLifetime = hostApplicationLifetime;
20+
}
21+
22+
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
23+
{
24+
using var activity = SActivitySource.StartActivity("Migrating database", ActivityKind.Client);
25+
26+
try
27+
{
28+
using var scope = _serviceProvider.CreateScope();
29+
var dbContext = scope.ServiceProvider.GetRequiredService<BoilerplateDbContext>();
30+
31+
await RunMigrationAsync(dbContext, cancellationToken);
32+
}
33+
catch (Exception ex)
34+
{
35+
activity?.AddException(ex);
36+
throw;
37+
}
38+
39+
_hostApplicationLifetime.StopApplication();
40+
}
41+
42+
private static async Task RunMigrationAsync(BoilerplateDbContext dbContext, CancellationToken cancellationToken)
43+
{
44+
var strategy = dbContext.Database.CreateExecutionStrategy();
45+
await strategy.ExecuteAsync(async () =>
46+
{
47+
// Run migration in a transaction to avoid partial migration if it fails.
48+
await dbContext.Database.MigrateAsync(cancellationToken);
49+
});
50+
}
51+
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.Hosting.Lifetime": "Information"
6+
}
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.Hosting.Lifetime": "Information"
6+
}
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsAspireSharedProject>true</IsAspireSharedProject>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<FrameworkReference Include="Microsoft.AspNetCore.App" />
12+
13+
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.3.0" />
14+
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="9.1.0" />
15+
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.11.2" />
16+
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.11.2" />
17+
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.11.1" />
18+
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.11.1" />
19+
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.11.1" />
20+
</ItemGroup>
21+
22+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
using Microsoft.AspNetCore.Builder;
2+
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Diagnostics.HealthChecks;
5+
using Microsoft.Extensions.Logging;
6+
using Microsoft.Extensions.ServiceDiscovery;
7+
using OpenTelemetry;
8+
using OpenTelemetry.Metrics;
9+
using OpenTelemetry.Trace;
10+
11+
namespace Microsoft.Extensions.Hosting;
12+
13+
// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
14+
// This project should be referenced by each service project in your solution.
15+
// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults
16+
public static class Extensions
17+
{
18+
public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
19+
{
20+
builder.ConfigureOpenTelemetry();
21+
22+
builder.AddDefaultHealthChecks();
23+
24+
builder.Services.AddServiceDiscovery();
25+
26+
builder.Services.ConfigureHttpClientDefaults(http =>
27+
{
28+
// Turn on resilience by default
29+
http.AddStandardResilienceHandler();
30+
31+
// Turn on service discovery by default
32+
http.AddServiceDiscovery();
33+
});
34+
35+
// Uncomment the following to restrict the allowed schemes for service discovery.
36+
// builder.Services.Configure<ServiceDiscoveryOptions>(options =>
37+
// {
38+
// options.AllowedSchemes = ["https"];
39+
// });
40+
41+
return builder;
42+
}
43+
44+
public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)
45+
{
46+
builder.Logging.AddOpenTelemetry(logging =>
47+
{
48+
logging.IncludeFormattedMessage = true;
49+
logging.IncludeScopes = true;
50+
});
51+
52+
builder.Services.AddOpenTelemetry()
53+
.WithMetrics(metrics =>
54+
{
55+
metrics.AddAspNetCoreInstrumentation()
56+
.AddHttpClientInstrumentation()
57+
.AddRuntimeInstrumentation();
58+
})
59+
.WithTracing(tracing =>
60+
{
61+
tracing.AddAspNetCoreInstrumentation()
62+
// Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
63+
//.AddGrpcClientInstrumentation()
64+
.AddHttpClientInstrumentation();
65+
});
66+
67+
builder.AddOpenTelemetryExporters();
68+
69+
return builder;
70+
}
71+
72+
private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)
73+
{
74+
var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
75+
76+
if (useOtlpExporter)
77+
{
78+
builder.Services.AddOpenTelemetry().UseOtlpExporter();
79+
}
80+
81+
// Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
82+
//if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
83+
//{
84+
// builder.Services.AddOpenTelemetry()
85+
// .UseAzureMonitor();
86+
//}
87+
88+
return builder;
89+
}
90+
91+
public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder)
92+
{
93+
builder.Services.AddHealthChecks()
94+
// Add a default liveness check to ensure app is responsive
95+
.AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
96+
97+
return builder;
98+
}
99+
100+
public static WebApplication MapDefaultEndpoints(this WebApplication app)
101+
{
102+
// Adding health checks endpoints to applications in non-development environments has security implications.
103+
// See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
104+
if (app.Environment.IsDevelopment())
105+
{
106+
// All health checks must pass for app to be considered ready to accept traffic after starting
107+
app.MapHealthChecks("/health");
108+
109+
// Only health checks tagged with the "live" tag must pass for app to be considered alive
110+
app.MapHealthChecks("/alive", new HealthCheckOptions
111+
{
112+
Predicate = r => r.Tags.Contains("live")
113+
});
114+
}
115+
116+
return app;
117+
}
118+
}

0 commit comments

Comments
 (0)