1using Microsoft.AspNetCore.Authorization;
2using Microsoft.AspNetCore.Builder;
3using Microsoft.AspNetCore.Diagnostics.HealthChecks;
4using Microsoft.Extensions.DependencyInjection;
5using Microsoft.Extensions.Diagnostics.HealthChecks;
6using Microsoft.Extensions.Logging;
7using Microsoft.Extensions.ServiceDiscovery;
8using Microsoft.OpenApi.Models;
9using OpenTelemetry;
10using OpenTelemetry.Metrics;
11using OpenTelemetry.Trace;
12
13#pragma warning disable IDE0130 // Namespace does not match folder structure
14namespace Microsoft.Extensions.Hosting;
15#pragma warning restore IDE0130 // Namespace does not match folder structure
16
17// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
18// This project should be referenced by each service project in your solution.
19// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults
20public static class Extensions
21{
22 private const string HealthEndpointPath = "/healthz";
23 private const string AlivenessEndpointPath = "/livez";
24
25 public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder, bool isApi = true) where TBuilder : IHostApplicationBuilder
26 {
27 builder.ConfigureOpenTelemetry();
28
29 builder.AddDefaultHealthChecks();
30
31 builder.Services.AddServiceDiscovery();
32
33 if (isApi)
34 {
35 builder.Services.AddOpenApi(options =>
36 {
37 options.AddDocumentTransformer((document, context, cancellationToken) =>
38 {
39 document.Components ??= new();
40 document.Components.SecuritySchemes ??= new Dictionary<string, OpenApiSecurityScheme>();
41 document.Components.SecuritySchemes["Bearer"] = new()
42 {
43 Type = SecuritySchemeType.Http,
44 Scheme = "bearer",
45 BearerFormat = "JWT",
46 Description = "JWT Authorization header using the Bearer scheme"
47 };
48
49 return Task.CompletedTask;
50 });
51
52 options.AddOperationTransformer((operation, context, cancellationToken) =>
53 {
54 var metadata = context.Description.ActionDescriptor.EndpointMetadata;
55
56 var hasAuthorize = metadata.OfType<AuthorizeAttribute>().Any();
57 var hasAllowAnonymous = metadata.OfType<AllowAnonymousAttribute>().Any();
58
59 if (hasAuthorize && !hasAllowAnonymous)
60 {
61 operation.Security = new List<OpenApiSecurityRequirement>
62 {
63 new()
64 {
65 [new OpenApiSecurityScheme
66 {
67 Reference = new OpenApiReference
68 {
69 Type = ReferenceType.SecurityScheme,
70 Id = "Bearer"
71 }
72 }] = Array.Empty<string>()
73 }
74 };
75 }
76
77 return Task.CompletedTask;
78 });
79 });
80 }
81
82 builder.Services.ConfigureHttpClientDefaults(http =>
83 {
84 // Turn on resilience by default
85 http.AddStandardResilienceHandler();
86
87 // Turn on service discovery by default
88 http.AddServiceDiscovery();
89 });
90
91 builder.Services.Configure<ServiceDiscoveryOptions>(options =>
92 options.AllowedSchemes = ["https"]);
93
94 return builder;
95 }
96
97 public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
98 {
99 builder.Logging.AddOpenTelemetry(logging =>
100 {
101 logging.IncludeFormattedMessage = true;
102 logging.IncludeScopes = true;
103 });
104
105 builder.Services.AddOpenTelemetry()
106 .WithMetrics(metrics =>
107 metrics.AddAspNetCoreInstrumentation()
108 .AddHttpClientInstrumentation()
109 .AddRuntimeInstrumentation()
110 )
111 .WithTracing(tracing =>
112 tracing.AddSource(builder.Environment.ApplicationName)
113 .AddAspNetCoreInstrumentation(tracing =>
114 // Exclude health check requests from tracing
115 tracing.Filter = context =>
116 !context.Request.Path.StartsWithSegments(HealthEndpointPath)
117 && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath)
118 )
119 // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
120 //.AddGrpcClientInstrumentation()
121 .AddHttpClientInstrumentation()
122 );
123
124 builder.AddOpenTelemetryExporters();
125
126 return builder;
127 }
128
129 private static TBuilder AddOpenTelemetryExporters<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
130 {
131 var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
132
133 if (useOtlpExporter)
134 {
135 builder.Services.AddOpenTelemetry().UseOtlpExporter();
136 }
137
138 return builder;
139 }
140
141 public static TBuilder AddDefaultHealthChecks<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
142 {
143 builder.Services.AddHealthChecks()
144 // Add a default liveness check to ensure app is responsive
145 .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
146
147 return builder;
148 }
149
150 public static WebApplication MapDefaultEndpoints(this WebApplication app)
151 {
152 // Adding health checks endpoints to applications in non-development environments has security implications.
153 // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
154 if (app.Environment.IsDevelopment())
155 {
156 app.MapOpenApi();
157
158 // All health checks must pass for app to be considered ready to accept traffic after starting
159 app.MapHealthChecks(HealthEndpointPath);
160
161 // Only health checks tagged with the "live" tag must pass for app to be considered alive
162 app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions
163 {
164 Predicate = r => r.Tags.Contains("live")
165 });
166 }
167
168 return app;
169 }
170}