Skip to content

Commit ad64141

Browse files
authored
Upgrade to .NET 10 and modernize project infrastructure (#48)
* Upgrade to .NET 10, API Versioning 10, and modernize code Upgraded all projects to .NET 10 (net10.0) and updated API Versioning packages to 10.0.0-preview.1. Overhauled .editorconfig for modern C# style, formatting, and naming conventions. Refactored codebase for .NET 10 compatibility, including property-based API Versioning usage, improved minimal API/controller patterns, and streamlined test host setup. Suppressed CA1707 in test projects. Performed general code cleanup, improved documentation, and ensured consistency across all files. * Replace .sln with .slnx XML-based solution file Removed the legacy Visual Studio .sln file and added a new .slnx solution file in XML format. The new file organizes projects and solution items into folders, providing a clearer and more structured project hierarchy. This change supports modern tooling and improves solution maintainability. * Upgrade tests to xUnit v3 and update async test patterns Replaced xUnit v2 packages with xunit.v3 and updated xunit.runner.visualstudio. Refactored test code to use TestContext.Current.CancellationToken for async HTTP calls, ensuring compatibility with xUnit v3. Removed xunit.runner.json (no longer needed in v3). Simplified global.json by removing msbuild-sdks. Added copilot-instructions.md to document FluentAssertions version constraint. Cleaned up unused usings and updated test.props references. * Update dependency versions and clarify package constraints Updated multiple package versions in Directory.Packages.props, including Azure.Core, Microsoft.Extensions.*, OpenTelemetry, and test dependencies. Revised copilot-instructions.md to clarify FluentAssertions upgrade limits and added a constraint for Swashbuckle.AspNetCore due to breaking changes in Microsoft.OpenApi v2. * Add Api.http with sample requests for API endpoint testing Added Api.http file containing example HTTP requests for /hello-world and /WeatherForecast endpoints, including variations with API versioning, custom actions, and resource IDs. This file aids in manual API testing and serves as documentation for available endpoints. * Switch sample to .NET Aspire Dashboard for metrics Replaced Docker Compose/Prometheus/Zipkin setup with .NET Aspire Dashboard for local metrics viewing. Updated README instructions and removed related config files to simplify observability setup. * Update service names and simplify dashboard run command Removed unnecessary env vars from run.cmd docker command. Updated OpenTelemetry resource serviceName values in Program.cs for clarity. Minor formatting and whitespace adjustments. * Retrieve logger from DI and fix catch block formatting Explicitly build the service provider and obtain an ILogger<Program> instance for logging. Also, adjust the catch block's closing brace for correct formatting. * Add minimal API SLI tests and improve SLI test coverage - Add ServiceLevelIndicatorMinimalApiTests for minimal API SLI scenarios, including custom operation names, route param extraction, and enrichment. - Refactor ServiceLevelIndicatorAspTests: centralize metric validation, set MeterListener callback in constructor, and add tests for 404/500 cases. - Fix typos in ServiceLevelIndicator and documentation (CustomerResourceId). - Add unit tests for default meter usage, Record overloads, and MeasuredOperation double-dispose. - Update README and controller XML docs for correct tag names and typos. - Add /server_error endpoint to TestController for 500 error SLI testing. * Enforce single [CustomerResourceId] per endpoint/action Add validation to prevent multiple [CustomerResourceId] attributes on a single endpoint or controller action, throwing InvalidOperationException if violated. Update middleware and tests to reflect this constraint, and allow AddAttribute to accept nullable values. Improve test coverage and error reporting for this scenario. * Add XML docs, logging, and code quality improvements - Add comprehensive XML documentation to public APIs for better IntelliSense and maintainability. - Log warnings in ServiceLevelIndicatorMiddleware when enrichments throw exceptions. - Refactor ServiceLevelIndicatorConvention to apply parameter metadata to all selectors and enforce single CustomerResourceIdAttribute. - Simplify ServiceLevelIndicatorOptions.Meter to an auto-property. - Validate required options in ServiceLevelIndicator constructor. - Apply minor code cleanups, formatting, and explicit access modifiers. - No breaking changes to the public API surface.
1 parent c095254 commit ad64141

75 files changed

Lines changed: 1345 additions & 961 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.editorconfig

Lines changed: 252 additions & 254 deletions
Large diffs are not rendered by default.

.github/copilot-instructions.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Copilot Instructions
2+
3+
## Package Constraints
4+
5+
- **FluentAssertions**: Do not upgrade beyond major version 7.x due to a licensing change in version 8+.
6+
- **Swashbuckle.AspNetCore**: Do not upgrade beyond version 6.x due to breaking changes in Microsoft.OpenApi v2.

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,4 +396,3 @@ FodyWeavers.xsd
396396

397397
# JetBrains Rider
398398
*.sln.iml
399-
/sample/DockerOpenTelemetry/output/logs.json

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<DefaultLanguage>en-US</DefaultLanguage>
99
<SolutionDir Condition=" '$(SolutionDir)' == '' OR '$(SolutionDir)' == '*Undefined if not building a solution or within Visual Studio*' ">$(MSBuildThisFileDirectory)</SolutionDir>
1010
<IsTestProject>$(MSBuildProjectName.EndsWith('.Tests'))</IsTestProject>
11-
<TargetFramework>net8.0</TargetFramework>
11+
<TargetFramework>net10.0</TargetFramework>
1212
<Nullable>enable</Nullable>
1313
<ImplicitUsings>enable</ImplicitUsings>
1414
<LangVersion>Latest</LangVersion>

Directory.Packages.props

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,31 @@
11
<Project>
22
<!-- Runtime -->
33
<ItemGroup>
4-
<PackageVersion Include="Asp.Versioning.Http" Version="8.1.0" />
5-
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.0" />
6-
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
7-
<PackageVersion Include="Azure.Core" Version="1.44.0" />
8-
<PackageVersion Include="DotNet.ReproducibleBuilds" Version="1.2.25" />
9-
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="7.0.3" />
10-
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="8.0.5" />
11-
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
12-
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
13-
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="8.0.1" />
14-
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
15-
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.6.143" />
16-
<PackageVersion Include="OpenTelemetry" Version="1.9.0" />
17-
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.9.0" />
18-
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
19-
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
4+
<PackageVersion Include="Asp.Versioning.Http" Version="10.0.0-preview.1" />
5+
<PackageVersion Include="Asp.Versioning.Mvc" Version="10.0.0-preview.1" />
6+
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="10.0.0-preview.1" />
7+
<PackageVersion Include="Azure.Core" Version="1.51.1" />
8+
<PackageVersion Include="DotNet.ReproducibleBuilds" Version="2.0.2" />
9+
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.3" />
10+
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="10.0.3" />
11+
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="10.0.3" />
12+
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
13+
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="10.0.3" />
14+
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.3" />
15+
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.9.50" />
16+
<PackageVersion Include="OpenTelemetry" Version="1.15.0" />
17+
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.15.0" />
18+
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.15.0" />
19+
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.0" />
2020
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.8.1" />
2121
</ItemGroup>
2222
<!-- Test -->
2323
<ItemGroup>
24-
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
25-
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
26-
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.2" />
27-
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
28-
<PackageVersion Include="xunit" Version="2.8.0" />
29-
<PackageVersion Include="xunit.categories" Version="2.0.6" />
30-
<PackageVersion Include="Xunit.DependencyInjection" Version="8.7.0" />
31-
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.0" />
24+
<PackageVersion Include="coverlet.collector" Version="8.0.0" />
25+
<PackageVersion Include="FluentAssertions" Version="7.2.0" />
26+
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.3" />
27+
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
28+
<PackageVersion Include="xunit.v3" Version="3.2.2" />
29+
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
3230
</ItemGroup>
3331
</Project>

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ By default, a meter named `ServiceLevelIndicator` with instrument name `operatio
4343
"Ok" when the http response status code is in the 2xx range,
4444
"Error" when the http response status code is in the 5xx range,
4545
"Unset" for any other status code.
46-
- http.response.status_code - The http status code.
46+
- http.response.status.code - The http status code.
4747
- http.request.method (Optional)- The http request method (GET, POST, etc) is added.
4848

4949
Difference between ServiceLevelIndicator and http.server.request.duration
@@ -304,12 +304,14 @@ An async version `EnrichAsync` is also available.
304304

305305
Try out the sample weather forecast Web API.
306306

307-
To view the metrics locally.
307+
To view the metrics locally using the [.NET Aspire Dashboard](https://aspire.dev/dashboard/standalone/):
308308

309-
1. Run Docker Desktop
310-
2. Run [sample\DockerOpenTelemetry\run.cmd](sample\DockerOpenTelemetry\run.cmd) to download and run zipkin and prometheus.
311-
3. Run the sample web API project and call the `GET WeatherForecast` using the Open API UI.
312-
4. You should see the SLI metrics in prometheus under the meter `operation_duration_milliseconds_bucket` where the `Operation = "GET WeatherForeCase"`, `http.response.status_code = 200`, `LocationId = "ms-loc://az/public/westus2"`, `activity.status_code = Ok`
309+
1. Start the Aspire dashboard:
310+
```
311+
docker run --rm -it -d -p 18888:18888 -p 4317:18889 -e DOTNET_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS=true -e DASHBOARD__OTLP__AUTHMODE=Unsecured --name aspire-dashboard mcr.microsoft.com/dotnet/aspire-dashboard:latest
312+
```
313+
2. Run the sample web API project and call the `GET WeatherForecast` using the Open API UI.
314+
3. Open `http://localhost:18888` to view the dashboard. You should see the SLI metrics under the meter `operation_duration_milliseconds_bucket` where the `Operation = "GET WeatherForecast"`, `http.response.status.code = 200`, `LocationId = "ms-loc://az/public/westus2"`, `activity.status.code = Ok`
313315
![SLI](assets/prometheus.jpg)
314-
5. If you run the sample with API Versioning, you will see something similar to the following.
316+
4. If you run the sample with API Versioning, you will see something similar to the following.
315317
![SLI](assets/versioned.jpg)

ServiceLevelIndicators.Asp.ApiVersioning/src/ApiVersionEnrichment.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
namespace ServiceLevelIndicators;
2+
23
using System.Threading;
34
using System.Threading.Tasks;
45
using Asp.Versioning;
@@ -15,7 +16,7 @@ public ValueTask EnrichAsync(WebEnrichmentContext context, CancellationToken can
1516

1617
private static string GetApiVersion(HttpContext context)
1718
{
18-
var apiVersioningFeature = context.ApiVersioningFeature();
19+
var apiVersioningFeature = context.ApiVersioningFeature;
1920
var versions = apiVersioningFeature.RawRequestedApiVersions;
2021
if (versions.Count == 1)
2122
return apiVersioningFeature.RequestedApiVersion?.ToString() ?? string.Empty;
@@ -26,4 +27,4 @@ private static string GetApiVersion(HttpContext context)
2627

2728
return "Unspecified";
2829
}
29-
}
30+
}

ServiceLevelIndicators.Asp.ApiVersioning/src/ServiceLevelIndicatorServiceCollectionExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ public static IServiceLevelIndicatorBuilder AddApiVersion(this IServiceLevelIndi
1111
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IEnrichment<WebEnrichmentContext>, ApiVersionEnrichment>());
1212
return builder;
1313
}
14-
}
14+
}

ServiceLevelIndicators.Asp.ApiVersioning/tests/ServiceLevelIndicatorVersionedAspTests.cs

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
namespace ServiceLevelIndicators.Asp.ApiVersioning.Tests;
1+
namespace ServiceLevelIndicators.Asp.ApiVersioning.Tests;
22

3+
using System.Diagnostics.Metrics;
4+
using System.Net;
35
using global::Asp.Versioning;
46
using Microsoft.AspNetCore.Builder;
57
using Microsoft.AspNetCore.Hosting;
68
using Microsoft.AspNetCore.TestHost;
79
using Microsoft.Extensions.DependencyInjection;
810
using Microsoft.Extensions.Hosting;
9-
using System.Diagnostics.Metrics;
10-
using System.Net;
11-
using Xunit.Abstractions;
1211

1312
public class ServiceLevelIndicatorVersionedAspTests : IDisposable
1413
{
@@ -57,7 +56,7 @@ public async Task SLI_Metrics_is_emitted_with_API_version_as_query_parameter()
5756
using var host = await CreateHost();
5857

5958
// Act
60-
var response = await host.GetTestClient().GetAsync("testSingle?api-version=2023-08-29");
59+
var response = await host.GetTestClient().GetAsync("testSingle?api-version=2023-08-29", TestContext.Current.CancellationToken);
6160

6261
// Assert
6362
response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -82,7 +81,7 @@ public async Task SLI_Metrics_is_emitted_with_API_version_as_header()
8281
httpClient.DefaultRequestHeaders.Add("api-version", "2023-08-29");
8382

8483
// Act
85-
var response = await httpClient.GetAsync("testSingle");
84+
var response = await httpClient.GetAsync("testSingle", TestContext.Current.CancellationToken);
8685

8786
// Assert
8887
response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -105,7 +104,7 @@ public async Task SLI_Metrics_is_emitted_with_neutral_API_version()
105104
using var host = await CreateHost();
106105

107106
// Act
108-
var response = await host.GetTestClient().GetAsync("testNeutral");
107+
var response = await host.GetTestClient().GetAsync("testNeutral", TestContext.Current.CancellationToken);
109108

110109
// Assert
111110
response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -128,7 +127,7 @@ public async Task SLI_Metrics_is_emitted_with_default_API_version()
128127
using var host = await CreateHostWithDefaultApiVersion();
129128

130129
// Act
131-
var response = await host.GetTestClient().GetAsync("testSingle");
130+
var response = await host.GetTestClient().GetAsync("testSingle", TestContext.Current.CancellationToken);
132131

133132
// Assert
134133
response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -142,7 +141,7 @@ public async Task Middleware_should_not_emit_metrics_for_nonexistent_route()
142141
using var host = await CreateHost();
143142

144143
// Act
145-
var response = await host.GetTestClient().GetAsync("does-not-exist?api-version=2023-08-29");
144+
var response = await host.GetTestClient().GetAsync("does-not-exist?api-version=2023-08-29", TestContext.Current.CancellationToken);
146145

147146
// Assert
148147
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
@@ -167,7 +166,7 @@ public async Task SLI_Metrics_is_emitted_when_api_version_is_invalid(string rout
167166
using var host = await CreateHost();
168167

169168
// Act
170-
var response = await host.GetTestClient().GetAsync(routeWithVersion);
169+
var response = await host.GetTestClient().GetAsync(routeWithVersion, TestContext.Current.CancellationToken);
171170

172171
// Assert
173172
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
@@ -176,9 +175,7 @@ public async Task SLI_Metrics_is_emitted_when_api_version_is_invalid(string rout
176175

177176
private async Task<IHost> CreateHost() =>
178177
await new HostBuilder()
179-
.ConfigureWebHost(webBuilder =>
180-
{
181-
webBuilder
178+
.ConfigureWebHost(webBuilder => webBuilder
182179
.UseTestServer()
183180
.ConfigureServices(services =>
184181
{
@@ -197,25 +194,19 @@ private async Task<IHost> CreateHost() =>
197194
.AddMvc()
198195
.AddApiVersion();
199196
})
200-
.Configure(app =>
201-
{
202-
app.UseRouting()
197+
.Configure(app => app.UseRouting()
203198
.UseServiceLevelIndicator()
204199
.Use(async (context, next) =>
205200
{
206201
await Task.Delay(MillisecondsDelay);
207202
await next(context);
208203
})
209-
.UseEndpoints(endpoints => endpoints.MapControllers());
210-
});
211-
})
204+
.UseEndpoints(endpoints => endpoints.MapControllers())))
212205
.StartAsync();
213206

214207
private async Task<IHost> CreateHostWithDefaultApiVersion() =>
215208
await new HostBuilder()
216-
.ConfigureWebHost(webBuilder =>
217-
{
218-
webBuilder
209+
.ConfigureWebHost(webBuilder => webBuilder
219210
.UseTestServer()
220211
.ConfigureServices(services =>
221212
{
@@ -236,18 +227,14 @@ private async Task<IHost> CreateHostWithDefaultApiVersion() =>
236227
.AddMvc()
237228
.AddApiVersion();
238229
})
239-
.Configure(app =>
240-
{
241-
app.UseRouting()
230+
.Configure(app => app.UseRouting()
242231
.UseServiceLevelIndicator()
243232
.Use(async (context, next) =>
244233
{
245234
await Task.Delay(MillisecondsDelay);
246235
await next(context);
247236
})
248-
.UseEndpoints(endpoints => endpoints.MapControllers());
249-
});
250-
})
237+
.UseEndpoints(endpoints => endpoints.MapControllers())))
251238
.StartAsync();
252239

253240
private void ValidateMetrics()
@@ -286,4 +273,4 @@ public void Dispose()
286273
Dispose(disposing: true);
287274
GC.SuppressFinalize(this);
288275
}
289-
}
276+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
namespace ServiceLevelIndicators.Asp.ApiVersioning.Tests;
22

3-
using Microsoft.AspNetCore.Mvc;
43
using global::Asp.Versioning;
4+
using Microsoft.AspNetCore.Mvc;
55

66
[ApiController]
77
[Route("[controller]")]
@@ -11,4 +11,4 @@ public class TestDoubleController : ControllerBase
1111
{
1212
[HttpGet]
1313
public IActionResult Get() => Ok("Hello World!");
14-
}
14+
}

0 commit comments

Comments
 (0)