From 00a3508e019167aeaddcb396f28a12fe2b8161cb Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Wed, 14 Oct 2026 20:13:23 -0500 Subject: [PATCH] Upgrade to .NET 11 RC2 Target net11.0 with SDK 11.0.100-rc.2.26504.105 across projects, CI, Docker images, and dev tooling, and move framework-aligned packages to 11.0.0-rc.2. Microsoft.AspNetCore.OpenApi 11 requires Microsoft.OpenApi 3.x, so adapt the transformers to the media type interfaces and keep the published document on OpenAPI 3.1. Refresh Foundatio, test SDK, and time provider testing packages. Drop the Microsoft.Extensions.Logging.Abstractions reference now that it ships in the shared framework, and disable static web assets in the test project so the Web and Job dotnet-watch assets added in RC2 don't collide. Co-Authored-By: Claude Opus 5.5 --- .codex/environments/environment.toml | 2 +- .devcontainer/devcontainer.json | 2 +- .github/workflows/build-arm64.yml | 2 +- .github/workflows/build.yaml | 9 +- .github/workflows/copilot-setup-steps.yml | 2 +- .github/workflows/elasticsearch-docker-8.yml | 2 +- .vscode/launch.json | 4 +- AGENTS.md | 2 +- Dockerfile | 10 +-- README.md | 2 +- build/EmailTemplatePreviews.cs | 2 +- global.json | 4 +- src/Directory.Build.props | 2 +- .../Exceptionless.AppHost.csproj | 2 +- .../Exceptionless.Core.csproj | 10 +-- .../Exceptionless.Insulation.csproj | 13 ++- .../Exceptionless.Web.csproj | 6 +- ...dpointDocumentationOperationTransformer.cs | 2 +- ...nlessOpenApiServiceCollectionExtensions.cs | 3 + .../RequestBodyContentOperationTransformer.cs | 6 +- .../Exceptionless.Tests/Api/Data/openapi.json | 90 +++++++++---------- .../Exceptionless.Tests.csproj | 9 +- .../Jobs/CloseInactiveSessionsJobTests.cs | 8 +- tests/ci/backend-shards.mjs | 2 +- 24 files changed, 100 insertions(+), 96 deletions(-) diff --git a/.codex/environments/environment.toml b/.codex/environments/environment.toml index 8b525d054a..273aa1d088 100644 --- a/.codex/environments/environment.toml +++ b/.codex/environments/environment.toml @@ -22,7 +22,7 @@ worktree_path="$PWD" echo "Setting up Exceptionless Aspire worktree: $worktree_path" if ! command -v dotnet >/dev/null 2>&1; then - echo "dotnet is not installed or not on PATH. Exceptionless requires .NET 10." >&2 + echo "dotnet is not installed or not on PATH. Exceptionless requires .NET 11." >&2 exit 1 fi diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f94530ef5f..44d5abd548 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -66,7 +66,7 @@ "version": "latest" }, "ghcr.io/devcontainers/features/dotnet:2": { - "version": "10.0" + "version": "11.0-preview" }, "ghcr.io/devcontainers/features/node:1": { "nodeGypDependencies": true, diff --git a/.github/workflows/build-arm64.yml b/.github/workflows/build-arm64.yml index 9bde92484d..e01c7e4a35 100644 --- a/.github/workflows/build-arm64.yml +++ b/.github/workflows/build-arm64.yml @@ -25,7 +25,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v6 with: - dotnet-version: 10.0.402 + dotnet-version: 11.0.100-rc.2.26504.105 - name: Build Reason env: GITHUB_EVENT: ${{ toJson(github) }} diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index f4c90bd753..ec19410c08 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -114,7 +114,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v6 with: - dotnet-version: 10.0.402 + dotnet-version: 11.0.100-rc.2.26504.105 - name: Version id: version @@ -156,7 +156,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v6 with: - dotnet-version: 10.0.402 + dotnet-version: 11.0.100-rc.2.26504.105 - uses: actions/cache@v6 with: @@ -215,7 +215,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v6 with: - dotnet-version: 10.0.402 + dotnet-version: 11.0.100-rc.2.26504.105 - name: Merge coverage run: | @@ -327,8 +327,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v6 with: - dotnet-version: 10.0.402 - dotnet-quality: ga + dotnet-version: 11.0.100-rc.2.26504.105 - name: Install Aspire CLI run: | diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index f534b8dd1c..d7ea3b9388 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -25,7 +25,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v6 with: - dotnet-version: 10.0.402 + dotnet-version: 11.0.100-rc.2.26504.105 - name: Install Aspire CLI run: | diff --git a/.github/workflows/elasticsearch-docker-8.yml b/.github/workflows/elasticsearch-docker-8.yml index 29da4456db..00c911eb5f 100644 --- a/.github/workflows/elasticsearch-docker-8.yml +++ b/.github/workflows/elasticsearch-docker-8.yml @@ -16,7 +16,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v6 with: - dotnet-version: 10.0.402 + dotnet-version: 11.0.100-rc.2.26504.105 - name: Build Reason env: GITHUB_EVENT: ${{ toJson(github) }} diff --git a/.vscode/launch.json b/.vscode/launch.json index b467cd8048..7fa3e5f201 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,7 +12,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceFolder}/src/Exceptionless.Web/bin/Debug/net10.0/Exceptionless.Web.dll", + "program": "${workspaceFolder}/src/Exceptionless.Web/bin/Debug/net11.0/Exceptionless.Web.dll", "args": [], "cwd": "${workspaceFolder}/src/Exceptionless.Web", "stopAtEntry": false, @@ -29,7 +29,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceFolder}/src/Exceptionless.Job/bin/Debug/net10.0/Exceptionless.Job.dll", + "program": "${workspaceFolder}/src/Exceptionless.Job/bin/Debug/net11.0/Exceptionless.Job.dll", "args": [], "cwd": "${workspaceFolder}", "stopAtEntry": false, diff --git a/AGENTS.md b/AGENTS.md index 1ec20f7afa..23915901a2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Exceptionless -Real-time error monitoring platform on .NET 10, Aspire, and Svelte 5. Keep changes readable, narrow, and backwards compatible. +Real-time error monitoring platform on .NET 11, Aspire, and Svelte 5. Keep changes readable, narrow, and backwards compatible. This file is root-scope agent guidance. When working in a subtree, read any nested `AGENTS.md` there too. Prefer nearby repo skills for detailed backend, frontend, testing, repository, and dogfood workflows. diff --git a/Dockerfile b/Dockerfile index 2e4084f47d..1f578ab187 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:10.0.402 AS build +FROM mcr.microsoft.com/dotnet/sdk:11.0.100-rc.2 AS build ARG MinVerVersionOverride WORKDIR /app @@ -40,7 +40,7 @@ RUN dotnet publish -c Release -o out --no-build # job -FROM mcr.microsoft.com/dotnet/aspnet:10.0.13 AS job +FROM mcr.microsoft.com/dotnet/aspnet:11.0.0-rc.2 AS job WORKDIR /app COPY --from=job-publish /app/src/Exceptionless.Job/out ./ @@ -59,7 +59,7 @@ RUN dotnet publish -c Release -o out --no-build /p:SkipSpaPublish=true # api -FROM mcr.microsoft.com/dotnet/aspnet:10.0.13 AS api +FROM mcr.microsoft.com/dotnet/aspnet:11.0.0-rc.2 AS api WORKDIR /app COPY --from=api-publish /app/src/Exceptionless.Web/out ./ @@ -77,7 +77,7 @@ RUN dotnet publish -c Release -o out --no-build # app -FROM mcr.microsoft.com/dotnet/aspnet:10.0.13 AS app +FROM mcr.microsoft.com/dotnet/aspnet:11.0.0-rc.2 AS app WORKDIR /app COPY --from=app-publish /app/src/Exceptionless.Web/out ./ @@ -145,7 +145,7 @@ USER elasticsearch RUN wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh && \ chmod +x dotnet-install.sh && \ - ./dotnet-install.sh --version 10.0.13 --runtime aspnetcore && \ + ./dotnet-install.sh --version 11.0.0-rc.2.26504.105 --runtime aspnetcore && \ rm dotnet-install.sh EXPOSE 8080 9200 diff --git a/README.md b/README.md index 46dd2eedd8..e53da947ee 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ _In appreciation for anyone who submits a non-trivial pull request, we will give Start here: 1. Read the [contributing document](https://github.com/exceptionless/Exceptionless/blob/main/CONTRIBUTING.md). -2. Install [Docker](https://www.docker.com/get-docker), [.NET 10.0](https://dotnet.microsoft.com/), and [Node 24+](https://nodejs.org/). +2. Install [Docker](https://www.docker.com/get-docker), [.NET 11.0](https://dotnet.microsoft.com/), and [Node 24+](https://nodejs.org/). 3. Run the app with one of these entry points: - Visual Studio Code: open the repo root and start the `Aspire` launch configuration. - Visual Studio: open `Exceptionless.slnx`, set `Exceptionless.AppHost` as the startup project, and run it. diff --git a/build/EmailTemplatePreviews.cs b/build/EmailTemplatePreviews.cs index cc0d07f70e..1e88ed5481 100644 --- a/build/EmailTemplatePreviews.cs +++ b/build/EmailTemplatePreviews.cs @@ -1,5 +1,5 @@ #:sdk Microsoft.NET.Sdk -#:property TargetFramework=net10.0 +#:property TargetFramework=net11.0 #:project ../src/Exceptionless.EmailTemplates/Exceptionless.EmailTemplates.csproj // Run from the repository root. diff --git a/global.json b/global.json index ab000ad016..7e2514b3e4 100644 --- a/global.json +++ b/global.json @@ -1,8 +1,8 @@ { "sdk": { - "version": "10.0.100", + "version": "11.0.100-rc.2.26504.105", "rollForward": "latestMinor", - "allowPrerelease": false + "allowPrerelease": true }, "test": { "runner": "Microsoft.Testing.Platform" diff --git a/src/Directory.Build.props b/src/Directory.Build.props index fa0f2d35de..dad7036eec 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,6 +1,6 @@ - net10.0 + net11.0 enable Exceptionless true diff --git a/src/Exceptionless.AppHost/Exceptionless.AppHost.csproj b/src/Exceptionless.AppHost/Exceptionless.AppHost.csproj index 335f84a5b3..c54dcd60df 100644 --- a/src/Exceptionless.AppHost/Exceptionless.AppHost.csproj +++ b/src/Exceptionless.AppHost/Exceptionless.AppHost.csproj @@ -1,7 +1,7 @@ Exe - net10.0 + net11.0 enable enable a9c2ddcc-e51d-4cd1-9782-96e1d74eec87 diff --git a/src/Exceptionless.Core/Exceptionless.Core.csproj b/src/Exceptionless.Core/Exceptionless.Core.csproj index 80adfbd569..c62aa7bd5c 100644 --- a/src/Exceptionless.Core/Exceptionless.Core.csproj +++ b/src/Exceptionless.Core/Exceptionless.Core.csproj @@ -7,14 +7,14 @@ - + - - - + + + - + diff --git a/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj b/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj index e63e29442f..bbb1b7bd0a 100644 --- a/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj +++ b/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj @@ -7,12 +7,11 @@ - - - - - - + + + + + @@ -20,7 +19,7 @@ - + diff --git a/src/Exceptionless.Web/Exceptionless.Web.csproj b/src/Exceptionless.Web/Exceptionless.Web.csproj index d33bbbd782..c72d585d3f 100644 --- a/src/Exceptionless.Web/Exceptionless.Web.csproj +++ b/src/Exceptionless.Web/Exceptionless.Web.csproj @@ -15,10 +15,8 @@ - - - + + diff --git a/src/Exceptionless.Web/Utility/OpenApi/EndpointDocumentationOperationTransformer.cs b/src/Exceptionless.Web/Utility/OpenApi/EndpointDocumentationOperationTransformer.cs index 169bb5f1d7..be09d9f00f 100644 --- a/src/Exceptionless.Web/Utility/OpenApi/EndpointDocumentationOperationTransformer.cs +++ b/src/Exceptionless.Web/Utility/OpenApi/EndpointDocumentationOperationTransformer.cs @@ -158,7 +158,7 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform requestBody.Required = true; if (requestBody.Content is not null) { - foreach (var mediaType in requestBody.Content.Values) + foreach (var mediaType in requestBody.Content.Values.OfType()) mediaType.Schema = RemoveNullAlternative(mediaType.Schema); } } diff --git a/src/Exceptionless.Web/Utility/OpenApi/ExceptionlessOpenApiServiceCollectionExtensions.cs b/src/Exceptionless.Web/Utility/OpenApi/ExceptionlessOpenApiServiceCollectionExtensions.cs index 9fe098e181..f46c76722a 100644 --- a/src/Exceptionless.Web/Utility/OpenApi/ExceptionlessOpenApiServiceCollectionExtensions.cs +++ b/src/Exceptionless.Web/Utility/OpenApi/ExceptionlessOpenApiServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi; namespace Exceptionless.Web.Utility.OpenApi; @@ -10,6 +11,8 @@ public static IServiceCollection AddExceptionlessOpenApi(this IServiceCollection { services.AddOpenApi(options => { + // ASP.NET Core 11 defaults to OpenAPI 3.2; keep publishing 3.1 for existing client generators. + options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_1; options.CreateSchemaReferenceId = SchemaReferenceIdHelper.CreateSchemaReferenceId; options.AddDocumentTransformer(); options.AddDocumentTransformer(); diff --git a/src/Exceptionless.Web/Utility/OpenApi/RequestBodyContentOperationTransformer.cs b/src/Exceptionless.Web/Utility/OpenApi/RequestBodyContentOperationTransformer.cs index f5dec66607..387cabf9f5 100644 --- a/src/Exceptionless.Web/Utility/OpenApi/RequestBodyContentOperationTransformer.cs +++ b/src/Exceptionless.Web/Utility/OpenApi/RequestBodyContentOperationTransformer.cs @@ -32,9 +32,9 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform operation.RequestBody = new OpenApiRequestBody { Required = true, - Content = new Dictionary + Content = new Dictionary { - ["multipart/form-data"] = new() + ["multipart/form-data"] = new OpenApiMediaType { Schema = CreateMultipartSchema(multipartFileUploadAttribute) } @@ -61,7 +61,7 @@ public Task TransformAsync(OpenApiOperation operation, OpenApiOperationTransform operation.RequestBody = new OpenApiRequestBody { Required = true, - Content = new Dictionary() + Content = new Dictionary() }; foreach (string contentType in contentTypes) diff --git a/tests/Exceptionless.Tests/Api/Data/openapi.json b/tests/Exceptionless.Tests/Api/Data/openapi.json index 17f157531e..89e7f3fe66 100644 --- a/tests/Exceptionless.Tests/Api/Data/openapi.json +++ b/tests/Exceptionless.Tests/Api/Data/openapi.json @@ -1,5 +1,5 @@ { - "openapi": "3.1.1", + "openapi": "3.1.2", "info": { "title": "Exceptionless API", "termsOfService": "https://exceptionless.com/terms/", @@ -16,7 +16,7 @@ }, "servers": [ { - "url": "http://localhost/" + "url": "http://localhost" } ], "paths": { @@ -1402,9 +1402,6 @@ "202": { "description": "Accepted" }, - "413": { - "description": "Payload Too Large" - }, "400": { "description": "No project id specified and no default project was found.", "content": { @@ -1424,6 +1421,9 @@ } } } + }, + "413": { + "description": "Payload Too Large" } }, "deprecated": true @@ -1478,9 +1478,6 @@ "202": { "description": "Accepted" }, - "413": { - "description": "Payload Too Large" - }, "400": { "description": "No project id specified and no default project was found.", "content": { @@ -1500,6 +1497,9 @@ } } } + }, + "413": { + "description": "Payload Too Large" } }, "deprecated": true @@ -3467,8 +3467,8 @@ } } }, - "409": { - "description": "The saved view preferences could not be updated due to a concurrent request.", + "404": { + "description": "The organization could not be found.", "content": { "application/problem\u002Bjson": { "schema": { @@ -3477,8 +3477,8 @@ } } }, - "404": { - "description": "The organization could not be found.", + "409": { + "description": "The saved view preferences could not be updated due to a concurrent request.", "content": { "application/problem\u002Bjson": { "schema": { @@ -3564,8 +3564,8 @@ } } }, - "409": { - "description": "The saved view preferences could not be updated due to a concurrent request.", + "404": { + "description": "The organization could not be found.", "content": { "application/problem\u002Bjson": { "schema": { @@ -3574,8 +3574,8 @@ } } }, - "404": { - "description": "The organization could not be found.", + "409": { + "description": "The saved view preferences could not be updated due to a concurrent request.", "content": { "application/problem\u002Bjson": { "schema": { @@ -4101,8 +4101,8 @@ } } }, - "422": { - "description": "The product tour name is invalid.", + "404": { + "description": "The current user could not be found.", "content": { "application/problem\u002Bjson": { "schema": { @@ -4111,8 +4111,8 @@ } } }, - "404": { - "description": "The current user could not be found.", + "422": { + "description": "The product tour name is invalid.", "content": { "application/problem\u002Bjson": { "schema": { @@ -7767,22 +7767,22 @@ "200": { "description": "OK" }, - "422": { - "description": "The requested status is invalid.", + "404": { + "description": "One or more stacks could not be found.", "content": { "application/problem\u002Bjson": { "schema": { - "$ref": "#/components/schemas/HttpValidationProblemDetails" + "$ref": "#/components/schemas/ProblemDetails" } } } }, - "404": { - "description": "One or more stacks could not be found.", + "422": { + "description": "The requested status is invalid.", "content": { "application/problem\u002Bjson": { "schema": { - "$ref": "#/components/schemas/ProblemDetails" + "$ref": "#/components/schemas/HttpValidationProblemDetails" } } } @@ -9093,9 +9093,6 @@ "202": { "description": "Accepted" }, - "413": { - "description": "Payload Too Large" - }, "400": { "description": "No project id specified and no default project was found.", "content": { @@ -9115,6 +9112,9 @@ } } } + }, + "413": { + "description": "Payload Too Large" } } } @@ -9465,9 +9465,6 @@ "202": { "description": "Accepted" }, - "413": { - "description": "Payload Too Large" - }, "400": { "description": "No project id specified and no default project was found.", "content": { @@ -9487,6 +9484,9 @@ } } } + }, + "413": { + "description": "Payload Too Large" } } } @@ -15161,43 +15161,43 @@ }, "tags": [ { - "name": "AssistantEndpoints" - }, - { - "name": "Project" + "name": "AdminEndpoints" }, { - "name": "AdminEndpoints" + "name": "AssistantEndpoints" }, { - "name": "OAuth" + "name": "Auth" }, { "name": "Event" }, { - "name": "Auth" + "name": "OAuth" }, { - "name": "Token" + "name": "Organization" }, { - "name": "WebHook" + "name": "Project" }, { "name": "SavedView" }, { - "name": "User" + "name": "Source Map" }, { - "name": "Organization" + "name": "Stack" }, { - "name": "Stack" + "name": "Token" }, { - "name": "Source Map" + "name": "User" + }, + { + "name": "WebHook" } ] } diff --git a/tests/Exceptionless.Tests/Exceptionless.Tests.csproj b/tests/Exceptionless.Tests/Exceptionless.Tests.csproj index d077bbf8ce..2c4d130a60 100644 --- a/tests/Exceptionless.Tests/Exceptionless.Tests.csproj +++ b/tests/Exceptionless.Tests/Exceptionless.Tests.csproj @@ -4,6 +4,9 @@ False false true + + false $(InterceptorsNamespaces);Microsoft.AspNetCore.OpenApi.Generated @@ -13,10 +16,10 @@ - - + + - + diff --git a/tests/Exceptionless.Tests/Jobs/CloseInactiveSessionsJobTests.cs b/tests/Exceptionless.Tests/Jobs/CloseInactiveSessionsJobTests.cs index ab17d7f345..ba2f586e20 100644 --- a/tests/Exceptionless.Tests/Jobs/CloseInactiveSessionsJobTests.cs +++ b/tests/Exceptionless.Tests/Jobs/CloseInactiveSessionsJobTests.cs @@ -164,15 +164,17 @@ public async Task CloseInactiveSessions(int defaultInactivePeriodInMinutes, bool Assert.Equal(2, events.Total); sessionStart = events.Documents.First(e => e.IsSessionStart()); - decimal sessionStartDuration = (decimal)(sessionHeartbeatUpdatedAgoInSeconds.HasValue ? (utcNow.SubtractSeconds(sessionHeartbeatUpdatedAgoInSeconds.Value) - sessionStart.Date.UtcDateTime).TotalSeconds : 0); + // Event values are persisted as doubles, so compare at that precision. + double sessionStartDuration = sessionHeartbeatUpdatedAgoInSeconds.HasValue ? (utcNow.SubtractSeconds(sessionHeartbeatUpdatedAgoInSeconds.Value) - sessionStart.Date.UtcDateTime).TotalSeconds : 0; + double? sessionStartValue = (double?)sessionStart.Value; if (willCloseSession) { - Assert.Equal(sessionStartDuration, sessionStart.Value); + Assert.Equal(sessionStartDuration, sessionStartValue); Assert.True(sessionStart.HasSessionEndTime()); } else { - Assert.Equal(sessionStartDuration, sessionStart.Value); + Assert.Equal(sessionStartDuration, sessionStartValue); Assert.False(sessionStart.HasSessionEndTime()); } } diff --git a/tests/ci/backend-shards.mjs b/tests/ci/backend-shards.mjs index fb2406616d..a6b4dddeb4 100644 --- a/tests/ci/backend-shards.mjs +++ b/tests/ci/backend-shards.mjs @@ -53,7 +53,7 @@ export function summarize(results) { } function run(args) { - const assembly = resolve(args.assembly ?? join(root, 'tests/Exceptionless.Tests/bin/Release/net10.0/Exceptionless.Tests.dll')); + const assembly = resolve(args.assembly ?? join(root, 'tests/Exceptionless.Tests/bin/Release/net11.0/Exceptionless.Tests.dll')); const discovered = JSON.parse( execFileSync('dotnet', [assembly, '--list-tests', 'json', '--no-ansi'], { encoding: 'utf8',