From cfa3dedf17fbf716226a735d1401e00ff53edee9 Mon Sep 17 00:00:00 2001 From: Donald Gray Date: Tue, 18 Aug 2026 14:15:20 +0100 Subject: [PATCH 1/7] Update to dotnet10 --- .github/workflows/run_build.yml | 2 +- Dockerfile.API | 4 ++-- Dockerfile.BackgroundHandler | 4 ++-- Dockerfile.Migrator | 4 ++-- global.json | 7 ++++++ .../API.Tests/API.Tests.csproj | 18 +++++++-------- src/IIIFPresentation/API/API.csproj | 22 +++++++++---------- .../API/Infrastructure/ServiceCollectionX.cs | 19 +++------------- .../AWS.Tests/AWS.Tests.csproj | 16 +++++++------- .../AWS.Tests/S3/S3ExtensionsTests.cs | 4 ++-- src/IIIFPresentation/AWS/AWS.csproj | 12 +++++----- .../AWS/Configuration/AWSConfiguration.cs | 4 +++- src/IIIFPresentation/AWS/S3/S3BucketWriter.cs | 2 +- src/IIIFPresentation/AWS/S3/S3Extensions.cs | 4 ++-- .../BackgroundHandler.Tests.csproj | 12 +++++----- .../BackgroundHandler.csproj | 6 ++--- .../Core.Tests/Core.Tests.csproj | 16 +++++++------- src/IIIFPresentation/Core/Core.csproj | 4 ++-- .../DLCS.Tests/DLCS.Tests.csproj | 14 ++++++------ src/IIIFPresentation/DLCS/DLCS.csproj | 12 +++++----- src/IIIFPresentation/Migrator/Migrator.csproj | 10 ++++----- .../Models.Tests/Models.Tests.csproj | 12 +++++----- src/IIIFPresentation/Models/Models.csproj | 2 +- .../Repository.Tests/Repository.Tests.csproj | 14 ++++++------ .../Repository/Repository.csproj | 10 ++++----- .../Services.Tests/Services.Tests.csproj | 10 ++++----- src/IIIFPresentation/Services/Services.csproj | 2 +- .../Integration/LocalStackFixture.cs | 10 +++++++-- .../Integration/PresentationContextFixture.cs | 15 +++++++++---- .../Test.Helpers/Test.Helpers.csproj | 20 ++++++++--------- 30 files changed, 150 insertions(+), 141 deletions(-) create mode 100644 global.json diff --git a/.github/workflows/run_build.yml b/.github/workflows/run_build.yml index 380df555b..51a9d788d 100644 --- a/.github/workflows/run_build.yml +++ b/.github/workflows/run_build.yml @@ -29,7 +29,7 @@ jobs: - id: setup-dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: "8.0.x" + dotnet-version: "10.0.x" - id: restore-dotnet-dependencies run: dotnet restore $SOLUTION - id: build-dotnet diff --git a/Dockerfile.API b/Dockerfile.API index bd5d2c6af..1336b50da 100644 --- a/Dockerfile.API +++ b/Dockerfile.API @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src COPY ["API/API.csproj", "API/"] @@ -15,7 +15,7 @@ RUN dotnet build "API.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "API.csproj" -c Release -r linux-x64 --self-contained false -o /app/publish -FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base ARG APP_VERSION=dev ENV APP_VERSION=${APP_VERSION} diff --git a/Dockerfile.BackgroundHandler b/Dockerfile.BackgroundHandler index 5763981ed..8e5f7caae 100644 --- a/Dockerfile.BackgroundHandler +++ b/Dockerfile.BackgroundHandler @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src COPY ["BackgroundHandler/BackgroundHandler.csproj", "BackgroundHandler/"] @@ -15,7 +15,7 @@ RUN dotnet build "BackgroundHandler.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "BackgroundHandler.csproj" -c Release -o /app/publish -FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base LABEL maintainer="Donald Gray , Jack Lewis " LABEL org.opencontainers.image.source=https://github.com/dlcs/iiif-presentation diff --git a/Dockerfile.Migrator b/Dockerfile.Migrator index 108de6bc3..8b0a05ab0 100644 --- a/Dockerfile.Migrator +++ b/Dockerfile.Migrator @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src COPY ["Migrator/Migrator.csproj", "Migrator/"] @@ -15,7 +15,7 @@ RUN dotnet build "Migrator.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "Migrator.csproj" -c Release -o /app/publish -FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base LABEL maintainer="Donald Gray , Jack Lewis " LABEL org.opencontainers.image.source=https://github.com/dlcs/iiif-presentation diff --git a/global.json b/global.json new file mode 100644 index 000000000..f4d27a352 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "10.0", + "rollForward": "latestMajor", + "allowPrerelease": false + } +} \ No newline at end of file diff --git a/src/IIIFPresentation/API.Tests/API.Tests.csproj b/src/IIIFPresentation/API.Tests/API.Tests.csproj index 13c38e211..287ad8fab 100644 --- a/src/IIIFPresentation/API.Tests/API.Tests.csproj +++ b/src/IIIFPresentation/API.Tests/API.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable @@ -10,15 +10,15 @@ - - - - - - + + + + + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/IIIFPresentation/API/API.csproj b/src/IIIFPresentation/API/API.csproj index 0e0aa5f6b..a353fbf2c 100644 --- a/src/IIIFPresentation/API/API.csproj +++ b/src/IIIFPresentation/API/API.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0 enable enable Linux @@ -9,22 +9,22 @@ - + - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - + + + + + + diff --git a/src/IIIFPresentation/API/Infrastructure/ServiceCollectionX.cs b/src/IIIFPresentation/API/Infrastructure/ServiceCollectionX.cs index 262cdeb1b..371049ac4 100644 --- a/src/IIIFPresentation/API/Infrastructure/ServiceCollectionX.cs +++ b/src/IIIFPresentation/API/Infrastructure/ServiceCollectionX.cs @@ -7,7 +7,7 @@ using AWS.Helpers; using AWS.S3; using MediatR; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Repository; using Sqids; @@ -117,22 +117,9 @@ public static IServiceCollection ConfigureSwagger(this IServiceCollection servic Description = "Basic Authorization header", }); - c.AddSecurityRequirement(new OpenApiSecurityRequirement + c.AddSecurityRequirement((document) => new OpenApiSecurityRequirement { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = "basic", - }, - Scheme = "basic", - Name = "Authorization", - In = ParameterLocation.Header - }, - [] - }, + [new OpenApiSecuritySchemeReference("basic", document)] = [] }); }); } diff --git a/src/IIIFPresentation/AWS.Tests/AWS.Tests.csproj b/src/IIIFPresentation/AWS.Tests/AWS.Tests.csproj index 9dff96606..ab8d84958 100644 --- a/src/IIIFPresentation/AWS.Tests/AWS.Tests.csproj +++ b/src/IIIFPresentation/AWS.Tests/AWS.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable @@ -10,13 +10,13 @@ - - - - - - - + + + + + + + diff --git a/src/IIIFPresentation/AWS.Tests/S3/S3ExtensionsTests.cs b/src/IIIFPresentation/AWS.Tests/S3/S3ExtensionsTests.cs index b22e66161..6448d83f5 100644 --- a/src/IIIFPresentation/AWS.Tests/S3/S3ExtensionsTests.cs +++ b/src/IIIFPresentation/AWS.Tests/S3/S3ExtensionsTests.cs @@ -21,7 +21,7 @@ public void AsObjectInBucket_Correct() ContentEncoding = "gzip", ContentLength = 132123, ContentType = "application/json", - ExpiresUtc = DateTime.UtcNow, + Expires = DateTime.UtcNow, ContentMD5 = "md5", }, ETag = "my-e-tag", @@ -38,7 +38,7 @@ public void AsObjectInBucket_Correct() objectFromBucket.Headers.ContentEncoding.Should().Be(getObjectResponse.Headers.ContentEncoding); objectFromBucket.Headers.ContentLength.Should().Be(getObjectResponse.Headers.ContentLength); objectFromBucket.Headers.ContentType.Should().Be(getObjectResponse.Headers.ContentType); - objectFromBucket.Headers.ExpiresUtc.Should().Be(getObjectResponse.Headers.ExpiresUtc); + objectFromBucket.Headers.ExpiresUtc.Should().Be(getObjectResponse.Headers.Expires); objectFromBucket.Headers.ContentMD5.Should().Be(getObjectResponse.Headers.ContentMD5); objectFromBucket.Headers.ETag.Should().Be(getObjectResponse.ETag); objectFromBucket.Headers.LastModified.Should().Be(getObjectResponse.LastModified); diff --git a/src/IIIFPresentation/AWS/AWS.csproj b/src/IIIFPresentation/AWS/AWS.csproj index a6ced17c7..5020f25ef 100644 --- a/src/IIIFPresentation/AWS/AWS.csproj +++ b/src/IIIFPresentation/AWS/AWS.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable @@ -13,11 +13,11 @@ - - - - - + + + + + diff --git a/src/IIIFPresentation/AWS/Configuration/AWSConfiguration.cs b/src/IIIFPresentation/AWS/Configuration/AWSConfiguration.cs index 7464636b7..56e1885df 100644 --- a/src/IIIFPresentation/AWS/Configuration/AWSConfiguration.cs +++ b/src/IIIFPresentation/AWS/Configuration/AWSConfiguration.cs @@ -74,7 +74,9 @@ public AwsBuilder WithAmazonS3(ServiceLifetime lifetime = ServiceLifetime.Single RegionEndpoint = RegionEndpoint.USEast1, ServiceURL = awsSettings.S3?.ServiceUrl.ThrowIfNullOrWhiteSpace(nameof(awsSettings.S3.ServiceUrl)), - ForcePathStyle = true + ForcePathStyle = true, + RequestChecksumCalculation = RequestChecksumCalculation.WHEN_REQUIRED, + ResponseChecksumValidation = ResponseChecksumValidation.WHEN_REQUIRED }; return new AmazonS3Client(new BasicAWSCredentials("foo", "bar"), amazonS3Config); }, lifetime); diff --git a/src/IIIFPresentation/AWS/S3/S3BucketWriter.cs b/src/IIIFPresentation/AWS/S3/S3BucketWriter.cs index 3546c2a64..530725f21 100644 --- a/src/IIIFPresentation/AWS/S3/S3BucketWriter.cs +++ b/src/IIIFPresentation/AWS/S3/S3BucketWriter.cs @@ -87,7 +87,7 @@ public async Task DeleteFolder(ObjectInBucket root, bool deleteRoot) listObjectsRequest.Marker = item.Key; } - } while (listObjectsResponse.IsTruncated); + } while (listObjectsResponse.IsTruncated == true); if (deleteObjectsRequest.Objects.Count > 0) { diff --git a/src/IIIFPresentation/AWS/S3/S3Extensions.cs b/src/IIIFPresentation/AWS/S3/S3Extensions.cs index 1e2591474..224f8e315 100644 --- a/src/IIIFPresentation/AWS/S3/S3Extensions.cs +++ b/src/IIIFPresentation/AWS/S3/S3Extensions.cs @@ -63,9 +63,9 @@ private static ObjectInBucketHeaders AsObjectInBucketHeaders(this GetObjectRespo ContentLength = headersCollection.ContentLength == -1L ? null : headersCollection.ContentLength, ContentMD5 = headersCollection.ContentMD5, ContentType = headersCollection.ContentType, - ExpiresUtc = headersCollection.ExpiresUtc, + ExpiresUtc = headersCollection.Expires, ETag = getObjectResponse.ETag, - LastModified = getObjectResponse.LastModified, + LastModified = getObjectResponse.LastModified.GetValueOrDefault(), }; return fromHeaders; } diff --git a/src/IIIFPresentation/BackgroundHandler.Tests/BackgroundHandler.Tests.csproj b/src/IIIFPresentation/BackgroundHandler.Tests/BackgroundHandler.Tests.csproj index 0a868c474..0be23bb41 100644 --- a/src/IIIFPresentation/BackgroundHandler.Tests/BackgroundHandler.Tests.csproj +++ b/src/IIIFPresentation/BackgroundHandler.Tests/BackgroundHandler.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable @@ -11,11 +11,11 @@ - - - - - + + + + + diff --git a/src/IIIFPresentation/BackgroundHandler/BackgroundHandler.csproj b/src/IIIFPresentation/BackgroundHandler/BackgroundHandler.csproj index e17a01e46..8ea7ffeb0 100644 --- a/src/IIIFPresentation/BackgroundHandler/BackgroundHandler.csproj +++ b/src/IIIFPresentation/BackgroundHandler/BackgroundHandler.csproj @@ -1,15 +1,15 @@  - net8.0 + net10.0 enable enable BackgroundHandler - - + + diff --git a/src/IIIFPresentation/Core.Tests/Core.Tests.csproj b/src/IIIFPresentation/Core.Tests/Core.Tests.csproj index 54fe7c809..8e71b02e8 100644 --- a/src/IIIFPresentation/Core.Tests/Core.Tests.csproj +++ b/src/IIIFPresentation/Core.Tests/Core.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable @@ -10,13 +10,13 @@ - - - - - - - + + + + + + + diff --git a/src/IIIFPresentation/Core/Core.csproj b/src/IIIFPresentation/Core/Core.csproj index c1a31588d..0270230fb 100644 --- a/src/IIIFPresentation/Core/Core.csproj +++ b/src/IIIFPresentation/Core/Core.csproj @@ -1,14 +1,14 @@  - net8.0 + net10.0 enable enable - + diff --git a/src/IIIFPresentation/DLCS.Tests/DLCS.Tests.csproj b/src/IIIFPresentation/DLCS.Tests/DLCS.Tests.csproj index 91b73beca..7becdd86e 100644 --- a/src/IIIFPresentation/DLCS.Tests/DLCS.Tests.csproj +++ b/src/IIIFPresentation/DLCS.Tests/DLCS.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable @@ -10,16 +10,16 @@ - + - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/IIIFPresentation/DLCS/DLCS.csproj b/src/IIIFPresentation/DLCS/DLCS.csproj index fdc48d8ec..ad44bc188 100644 --- a/src/IIIFPresentation/DLCS/DLCS.csproj +++ b/src/IIIFPresentation/DLCS/DLCS.csproj @@ -1,17 +1,17 @@  - net8.0 + net10.0 enable enable - - - - - + + + + + diff --git a/src/IIIFPresentation/Migrator/Migrator.csproj b/src/IIIFPresentation/Migrator/Migrator.csproj index 8251d51d2..a6bea496d 100644 --- a/src/IIIFPresentation/Migrator/Migrator.csproj +++ b/src/IIIFPresentation/Migrator/Migrator.csproj @@ -2,17 +2,17 @@ Exe - net8.0 + net10.0 enable enable Linux - - - - + + + + diff --git a/src/IIIFPresentation/Models.Tests/Models.Tests.csproj b/src/IIIFPresentation/Models.Tests/Models.Tests.csproj index dc315816d..1324a8f94 100644 --- a/src/IIIFPresentation/Models.Tests/Models.Tests.csproj +++ b/src/IIIFPresentation/Models.Tests/Models.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable @@ -10,14 +10,14 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/IIIFPresentation/Models/Models.csproj b/src/IIIFPresentation/Models/Models.csproj index e05154f8f..2ef69ec28 100644 --- a/src/IIIFPresentation/Models/Models.csproj +++ b/src/IIIFPresentation/Models/Models.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable Linux diff --git a/src/IIIFPresentation/Repository.Tests/Repository.Tests.csproj b/src/IIIFPresentation/Repository.Tests/Repository.Tests.csproj index a5608c2bb..3f61eb386 100644 --- a/src/IIIFPresentation/Repository.Tests/Repository.Tests.csproj +++ b/src/IIIFPresentation/Repository.Tests/Repository.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable @@ -10,15 +10,15 @@ - - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/IIIFPresentation/Repository/Repository.csproj b/src/IIIFPresentation/Repository/Repository.csproj index 305d5efd9..3883eae0c 100644 --- a/src/IIIFPresentation/Repository/Repository.csproj +++ b/src/IIIFPresentation/Repository/Repository.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable Linux @@ -9,11 +9,11 @@ - + - - - + + + diff --git a/src/IIIFPresentation/Services.Tests/Services.Tests.csproj b/src/IIIFPresentation/Services.Tests/Services.Tests.csproj index aa3249d8e..7c118edea 100644 --- a/src/IIIFPresentation/Services.Tests/Services.Tests.csproj +++ b/src/IIIFPresentation/Services.Tests/Services.Tests.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable Services.Tests @@ -13,10 +13,10 @@ - - - - + + + + diff --git a/src/IIIFPresentation/Services/Services.csproj b/src/IIIFPresentation/Services/Services.csproj index 64bca5fe4..497cfcf7a 100644 --- a/src/IIIFPresentation/Services/Services.csproj +++ b/src/IIIFPresentation/Services/Services.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 enable enable Services diff --git a/src/IIIFPresentation/Test.Helpers/Integration/LocalStackFixture.cs b/src/IIIFPresentation/Test.Helpers/Integration/LocalStackFixture.cs index 41b3d63c9..c66b722e2 100644 --- a/src/IIIFPresentation/Test.Helpers/Integration/LocalStackFixture.cs +++ b/src/IIIFPresentation/Test.Helpers/Integration/LocalStackFixture.cs @@ -32,7 +32,11 @@ public LocalStackFixture() .WithEnvironment("SERVICES", "s3,sqs,sns") .WithEnvironment("DOCKER_HOST", "unix:///var/run/docker.sock") .WithEnvironment("DEBUG", "1") - .WithPortBinding(0, LocalStackContainerPort); + .WithPortBinding(0, LocalStackContainerPort) + // The port accepts connections well before LocalStack's internal services have + // finished booting; wait for the health endpoint to actually respond instead. + .WithWaitStrategy(Wait.ForUnixContainer() + .UntilHttpRequestIsSucceeded(request => request.ForPath("/_localstack/health").ForPort(LocalStackContainerPort))); localStackContainer = localStackBuilder.Build(); } @@ -60,7 +64,9 @@ private void SetAWSClientFactories() RegionEndpoint = RegionEndpoint.EUWest1, UseHttp = true, ForcePathStyle = true, - ServiceURL = localStackUrl + ServiceURL = localStackUrl, + RequestChecksumCalculation = RequestChecksumCalculation.WHEN_REQUIRED, + ResponseChecksumValidation = ResponseChecksumValidation.WHEN_REQUIRED }; AWSS3ClientFactory = () => new AmazonS3Client(new BasicAWSCredentials("foo", "bar"), s3Config); diff --git a/src/IIIFPresentation/Test.Helpers/Integration/PresentationContextFixture.cs b/src/IIIFPresentation/Test.Helpers/Integration/PresentationContextFixture.cs index 39a4440ff..59669d35b 100644 --- a/src/IIIFPresentation/Test.Helpers/Integration/PresentationContextFixture.cs +++ b/src/IIIFPresentation/Test.Helpers/Integration/PresentationContextFixture.cs @@ -355,18 +355,25 @@ private void SetPropertiesFromContainer(ICustomerIdProvider customerIdProvider) } private const string PreservedCollections = - "'root','FirstChildCollection','SecondChildCollection', 'NonPublic', 'IiifCollection'"; + "'root','FirstChildCollection','SecondChildCollection', 'NonPublic', 'IiifCollection', 'IiifCollectionWithItems'"; public void CleanUp() { + // Cleanup covers every customer, including customer 1: tests create collections/manifests + // directly via DbContext, and those default to customer 1 unless a test overrides it. // Remove hierarchy rows hanging off a collection that's about to be deleted first. // hierarchy.parent=>collections does not cascade delete, hierarcy.collection_id=>collections does DbContext.Database.ExecuteSqlRaw( - $"DELETE FROM hierarchy WHERE customer_id != 1 AND parent IN (SELECT id FROM collections WHERE customer_id != 1 AND id NOT IN ({PreservedCollections}))"); + $"DELETE FROM hierarchy WHERE parent IN (SELECT id FROM collections WHERE id NOT IN ({PreservedCollections}))"); DbContext.Database.ExecuteSqlRaw( - $"DELETE FROM collections WHERE customer_id != 1 AND id NOT IN ({PreservedCollections})"); + $"DELETE FROM collections WHERE id NOT IN ({PreservedCollections})"); DbContext.Database.ExecuteSqlRaw( - "DELETE FROM manifests WHERE customer_id != 1 AND id NOT IN ('FirstChildManifest', 'FirstChildManifestProcessing')"); + "DELETE FROM manifests WHERE id NOT IN ('FirstChildManifest', 'FirstChildManifestProcessing')"); + + // The raw SQL deletes above bypass the change tracker, so DbContext (a singleton shared across + // every test in the collection) can be left holding stale references to now-deleted rows. Clear it + // so a later test's SaveChangesAsync doesn't try to flush changes against rows that no longer exist. + DbContext.ChangeTracker.Clear(); } } diff --git a/src/IIIFPresentation/Test.Helpers/Test.Helpers.csproj b/src/IIIFPresentation/Test.Helpers/Test.Helpers.csproj index ace3b3e4c..f10204c2a 100644 --- a/src/IIIFPresentation/Test.Helpers/Test.Helpers.csproj +++ b/src/IIIFPresentation/Test.Helpers/Test.Helpers.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable @@ -10,15 +10,15 @@ - - - - - - - - - + + + + + + + + + From 2234dbaa7f4ced31ef47a9e1e090d343d1c405cd Mon Sep 17 00:00:00 2001 From: Donald Gray Date: Tue, 18 Aug 2026 15:31:41 +0100 Subject: [PATCH 2/7] Switch from mediatr to mediator lib Minimal changes, same functionality. --- src/IIIFPresentation/API/API.csproj | 5 +++++ .../Features/Manifest/ManifestController.cs | 2 +- .../Requests/CreateHierarchicalManifest.cs | 4 ++-- .../Manifest/Requests/CreateManifest.cs | 4 ++-- .../Manifest/Requests/DeleteManifest.cs | 4 ++-- .../Features/Manifest/Requests/GetManifest.cs | 8 ++++---- .../Requests/GetManifestHierarchical.cs | 4 ++-- .../Requests/UpsertHierarchicalManifest.cs | 4 ++-- .../Manifest/Requests/UpsertManifest.cs | 4 ++-- .../Features/Storage/CollectionController.cs | 2 +- .../Storage/Requests/CreateCollection.cs | 4 ++-- .../Requests/CreateHierarchicalCollection.cs | 4 ++-- .../Storage/Requests/DeleteCollection.cs | 4 ++-- .../Storage/Requests/GetCollection.cs | 4 ++-- .../Requests/GetHierarchicalCollection.cs | 4 ++-- .../Storage/Requests/SearchCollection.cs | 4 ++-- .../Storage/Requests/UpsertCollection.cs | 4 ++-- .../Requests/UpsertHierarchicalCollection.cs | 4 ++-- .../API/Features/Storage/StorageController.cs | 2 +- .../Infrastructure/PresentationController.cs | 6 +++--- .../Pipelines/CacheInvalidationBehaviour.cs | 10 +++++----- .../Pipelines}/LoggingBehaviour.cs | 10 +++++----- .../API/Infrastructure/ServiceCollectionX.cs | 20 ++++++++++--------- src/IIIFPresentation/API/Program.cs | 2 +- .../Repository/Repository.csproj | 1 - 25 files changed, 65 insertions(+), 59 deletions(-) rename src/IIIFPresentation/API/Infrastructure/{Mediatr/Behaviours => Requests/Pipelines}/LoggingBehaviour.cs (74%) diff --git a/src/IIIFPresentation/API/API.csproj b/src/IIIFPresentation/API/API.csproj index a353fbf2c..cff4b2e86 100644 --- a/src/IIIFPresentation/API/API.csproj +++ b/src/IIIFPresentation/API/API.csproj @@ -13,6 +13,11 @@ + + + all + runtime; build; native; contentfiles; analyzers + diff --git a/src/IIIFPresentation/API/Features/Manifest/ManifestController.cs b/src/IIIFPresentation/API/Features/Manifest/ManifestController.cs index fb407e817..58bbb24a4 100644 --- a/src/IIIFPresentation/API/Features/Manifest/ManifestController.cs +++ b/src/IIIFPresentation/API/Features/Manifest/ManifestController.cs @@ -10,7 +10,7 @@ using API.Infrastructure.Requests; using API.Settings; using Models.API.General; -using MediatR; +using Mediator; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; diff --git a/src/IIIFPresentation/API/Features/Manifest/Requests/CreateHierarchicalManifest.cs b/src/IIIFPresentation/API/Features/Manifest/Requests/CreateHierarchicalManifest.cs index 2c3943baf..144d18734 100644 --- a/src/IIIFPresentation/API/Features/Manifest/Requests/CreateHierarchicalManifest.cs +++ b/src/IIIFPresentation/API/Features/Manifest/Requests/CreateHierarchicalManifest.cs @@ -3,7 +3,7 @@ using API.Helpers; using API.Infrastructure.Requests; using API.Settings; -using MediatR; +using Mediator; using Microsoft.Extensions.Options; using Services.Manifests.Settings; @@ -37,7 +37,7 @@ public class CreateHierarchicalManifestHandler( IOptions apiOptions) : IRequestHandler { - public async Task Handle(CreateHierarchicalManifest request, + public async ValueTask Handle(CreateHierarchicalManifest request, CancellationToken cancellationToken) { var (error, context) = await hierarchicalRequestHelper.PrepareForCreate( diff --git a/src/IIIFPresentation/API/Features/Manifest/Requests/CreateManifest.cs b/src/IIIFPresentation/API/Features/Manifest/Requests/CreateManifest.cs index 70062d870..d46f7ecc5 100644 --- a/src/IIIFPresentation/API/Features/Manifest/Requests/CreateManifest.cs +++ b/src/IIIFPresentation/API/Features/Manifest/Requests/CreateManifest.cs @@ -1,6 +1,6 @@ using API.Helpers; using API.Infrastructure.Requests; -using MediatR; +using Mediator; using Models.API.Manifest; namespace API.Features.Manifest.Requests; @@ -24,7 +24,7 @@ public class CreateManifest( public class CreateManifestHandler(IManifestWrite manifestService, IRequestIdResolver requestIdResolver) : IRequestHandler { - public async Task Handle(CreateManifest request, CancellationToken cancellationToken) + public async ValueTask Handle(CreateManifest request, CancellationToken cancellationToken) { var (error, resolvedId) = requestIdResolver.ResolveAndValidate(request.CustomerId, request.PresentationManifest.Id); diff --git a/src/IIIFPresentation/API/Features/Manifest/Requests/DeleteManifest.cs b/src/IIIFPresentation/API/Features/Manifest/Requests/DeleteManifest.cs index 7197eddcf..409c8b2c3 100644 --- a/src/IIIFPresentation/API/Features/Manifest/Requests/DeleteManifest.cs +++ b/src/IIIFPresentation/API/Features/Manifest/Requests/DeleteManifest.cs @@ -1,7 +1,7 @@ using API.Features.Common.Helpers; using API.Features.Storage.Helpers; using Core; -using MediatR; +using Mediator; using Models.API.General; using Repository; @@ -20,7 +20,7 @@ public class DeleteManifestHandler( ILogger logger) : IRequestHandler> { - public async Task> Handle(DeleteManifest request, + public async ValueTask> Handle(DeleteManifest request, CancellationToken cancellationToken) { logger.LogDebug("Deleting manifest {ManifestId} for customer {CustomerId}", request.ManifestId, diff --git a/src/IIIFPresentation/API/Features/Manifest/Requests/GetManifest.cs b/src/IIIFPresentation/API/Features/Manifest/Requests/GetManifest.cs index 08ae8fde0..f0d8f7731 100644 --- a/src/IIIFPresentation/API/Features/Manifest/Requests/GetManifest.cs +++ b/src/IIIFPresentation/API/Features/Manifest/Requests/GetManifest.cs @@ -1,7 +1,7 @@ using System.Collections.Immutable; using API.Infrastructure.Helpers; using API.Infrastructure.Requests; -using MediatR; +using Mediator; using Microsoft.Extensions.Primitives; using Models.API.Manifest; @@ -26,8 +26,8 @@ public class GetManifest( public class GetManifestHandler(IManifestRead manifestRead) : IRequestHandler> { - public Task> Handle(GetManifest request, + public ValueTask> Handle(GetManifest request, CancellationToken cancellationToken) - => manifestRead.GetManifest(request.CustomerId, request.Id, request.IfNoneMatch, request.PathOnly, - cancellationToken); + => new(manifestRead.GetManifest(request.CustomerId, request.Id, request.IfNoneMatch, request.PathOnly, + cancellationToken)); } diff --git a/src/IIIFPresentation/API/Features/Manifest/Requests/GetManifestHierarchical.cs b/src/IIIFPresentation/API/Features/Manifest/Requests/GetManifestHierarchical.cs index a0810f1b1..ecf19bc7e 100644 --- a/src/IIIFPresentation/API/Features/Manifest/Requests/GetManifestHierarchical.cs +++ b/src/IIIFPresentation/API/Features/Manifest/Requests/GetManifestHierarchical.cs @@ -3,7 +3,7 @@ using AWS.S3; using AWS.Settings; using Core.Streams; -using MediatR; +using Mediator; using Microsoft.Extensions.Options; using Models.Database.General; using Repository.Paths; @@ -24,7 +24,7 @@ public class GetManifestHierarchicalHandler( { private readonly AWSSettings settings = options.Value; - public async Task Handle(GetManifestHierarchical request, + public async ValueTask Handle(GetManifestHierarchical request, CancellationToken cancellationToken) { var manifest = request.Hierarchy.Manifest ?? diff --git a/src/IIIFPresentation/API/Features/Manifest/Requests/UpsertHierarchicalManifest.cs b/src/IIIFPresentation/API/Features/Manifest/Requests/UpsertHierarchicalManifest.cs index 1f80b9a78..ebce1a270 100644 --- a/src/IIIFPresentation/API/Features/Manifest/Requests/UpsertHierarchicalManifest.cs +++ b/src/IIIFPresentation/API/Features/Manifest/Requests/UpsertHierarchicalManifest.cs @@ -3,7 +3,7 @@ using API.Helpers; using API.Infrastructure.Requests; using API.Settings; -using MediatR; +using Mediator; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Models.API.Manifest; @@ -43,7 +43,7 @@ public class UpsertHierarchicalManifestHandler( IOptions apiOptions) : IRequestHandler { - public async Task Handle(UpsertHierarchicalManifest request, + public async ValueTask Handle(UpsertHierarchicalManifest request, CancellationToken cancellationToken) { var (error, context) = await hierarchicalRequestHelper.PrepareForUpsert( diff --git a/src/IIIFPresentation/API/Features/Manifest/Requests/UpsertManifest.cs b/src/IIIFPresentation/API/Features/Manifest/Requests/UpsertManifest.cs index 9911850f6..29dbe2a31 100644 --- a/src/IIIFPresentation/API/Features/Manifest/Requests/UpsertManifest.cs +++ b/src/IIIFPresentation/API/Features/Manifest/Requests/UpsertManifest.cs @@ -1,6 +1,6 @@ using API.Helpers; using API.Infrastructure.Requests; -using MediatR; +using Mediator; using Microsoft.Extensions.Primitives; using Models.API.Manifest; @@ -28,7 +28,7 @@ public class UpsertManifest( public class UpsertManifestHandler(IManifestWrite manifestService, IRequestIdResolver requestIdResolver) : IRequestHandler { - public async Task Handle(UpsertManifest request, CancellationToken cancellationToken) + public async ValueTask Handle(UpsertManifest request, CancellationToken cancellationToken) { var (error, resolvedId) = requestIdResolver.ResolveAndValidate(request.CustomerId, request.PresentationManifest.Id, request.ManifestId); diff --git a/src/IIIFPresentation/API/Features/Storage/CollectionController.cs b/src/IIIFPresentation/API/Features/Storage/CollectionController.cs index 4ac1df83d..2a7e9b77b 100644 --- a/src/IIIFPresentation/API/Features/Storage/CollectionController.cs +++ b/src/IIIFPresentation/API/Features/Storage/CollectionController.cs @@ -11,7 +11,7 @@ using API.Infrastructure.Requests; using API.Settings; using Core.Helpers; -using MediatR; +using Mediator; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs index 1ffe480ba..d265c4e04 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs @@ -1,6 +1,6 @@ using API.Helpers; using API.Infrastructure.Requests; -using MediatR; +using Mediator; using Models.API.Collection; namespace API.Features.Storage.Requests; @@ -21,7 +21,7 @@ public class CreateCollection(int customerId, PresentationCollection collection, public class CreateCollectionHandler(ICollectionWrite collectionService, IRequestIdResolver requestIdResolver) : IRequestHandler { - public async Task Handle(CreateCollection request, CancellationToken cancellationToken) + public async ValueTask Handle(CreateCollection request, CancellationToken cancellationToken) { var (error, resolvedId) = requestIdResolver.ResolveAndValidate(request.CustomerId, request.Collection.Id); if (error != null) return error; diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/CreateHierarchicalCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/CreateHierarchicalCollection.cs index edce151a4..bd570c3e6 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/CreateHierarchicalCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/CreateHierarchicalCollection.cs @@ -3,7 +3,7 @@ using API.Helpers; using API.Infrastructure.Requests; using API.Settings; -using MediatR; +using Mediator; using Microsoft.Extensions.Options; using Models.API.Collection; @@ -35,7 +35,7 @@ public class CreateHierarchicalCollectionHandler( IOptions apiOptions) : IRequestHandler { - public async Task Handle(CreateHierarchicalCollection request, + public async ValueTask Handle(CreateHierarchicalCollection request, CancellationToken cancellationToken) { var (error, context) = await hierarchicalRequestHelper.PrepareForCreate( diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/DeleteCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/DeleteCollection.cs index 8bc0a57c6..499be110b 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/DeleteCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/DeleteCollection.cs @@ -1,7 +1,7 @@ using API.Features.Common.Helpers; using API.Features.Storage.Helpers; using Core; -using MediatR; +using Mediator; using Models; using Models.API.General; using Models.Database.Collections; @@ -23,7 +23,7 @@ public class DeleteCollectionHandler( ILogger logger) : IRequestHandler> { - public async Task> Handle(DeleteCollection request, CancellationToken cancellationToken) + public async ValueTask> Handle(DeleteCollection request, CancellationToken cancellationToken) { logger.LogDebug("Deleting collection {CollectionId} for customer {CustomerId}", request.CollectionId, request.CustomerId); diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs index eb2105a4f..5f20728ca 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs @@ -5,7 +5,7 @@ using API.Infrastructure.Requests; using API.Settings; using AWS.Helpers; -using MediatR; +using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Models.API.Collection; @@ -39,7 +39,7 @@ public class GetCollectionHandler(PresentationContext dbContext, IIIIFS3Service SettingsBasedPathGenerator settingsBasedPathGenerator, IOptions options) : IRequestHandler> { - public async Task> Handle(GetCollection request, + public async ValueTask> Handle(GetCollection request, CancellationToken cancellationToken) { var collection = await dbContext.RetrieveCollectionWithParentAsync(request.Id, diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs index 8762dffb5..68074ea5a 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs @@ -7,7 +7,7 @@ using AWS.Settings; using Core.Streams; using IIIF.Presentation.V3; -using MediatR; +using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Models.Database.General; @@ -32,7 +32,7 @@ public class GetHierarchicalCollectionHandler( { private readonly AWSSettings settings = options.Value; - public async Task Handle(GetHierarchicalCollection request, + public async ValueTask Handle(GetHierarchicalCollection request, CancellationToken cancellationToken) { if (request.Hierarchy.CollectionId == null || request.Hierarchy.Collection == null) diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/SearchCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/SearchCollection.cs index 72d61f52b..144167f52 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/SearchCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/SearchCollection.cs @@ -4,7 +4,7 @@ using API.Features.Storage.Models; using API.Infrastructure.Requests; using API.Settings; -using MediatR; +using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Models.API.Collection; @@ -51,7 +51,7 @@ public class SearchCollectionHandler( { private readonly ApiSettings settings = options.Value; - public async Task> Handle(SearchCollection request, + public async ValueTask> Handle(SearchCollection request, CancellationToken cancellationToken) { // Only the collection itself is required - the search result is synthetic, so it carries none of the diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs index 8e06a7bb0..0ed20e00d 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs @@ -1,6 +1,6 @@ using API.Helpers; using API.Infrastructure.Requests; -using MediatR; +using Mediator; using Models.API.Collection; namespace API.Features.Storage.Requests; @@ -25,7 +25,7 @@ public class UpsertCollection(int customerId, string collectionId, PresentationC public class UpsertCollectionHandler(ICollectionWrite collectionService, IRequestIdResolver requestIdResolver) : IRequestHandler { - public async Task Handle(UpsertCollection request, CancellationToken cancellationToken) + public async ValueTask Handle(UpsertCollection request, CancellationToken cancellationToken) { var (error, resolvedId) = requestIdResolver.ResolveAndValidate(request.CustomerId, request.Collection.Id, request.CollectionId); diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/UpsertHierarchicalCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/UpsertHierarchicalCollection.cs index cdebb58e8..9d18da9e7 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/UpsertHierarchicalCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/UpsertHierarchicalCollection.cs @@ -3,7 +3,7 @@ using API.Features.Storage.Validators; using API.Infrastructure.Requests; using API.Settings; -using MediatR; +using Mediator; using Microsoft.Extensions.Options; using Models.API.Collection; using DbCollection = Models.Database.Collections.Collection; @@ -38,7 +38,7 @@ public class UpsertHierarchicalCollectionHandler( IOptions apiOptions) : IRequestHandler { - public async Task Handle(UpsertHierarchicalCollection request, + public async ValueTask Handle(UpsertHierarchicalCollection request, CancellationToken cancellationToken) { var (error, context) = await hierarchicalRequestHelper.PrepareForUpsert( diff --git a/src/IIIFPresentation/API/Features/Storage/StorageController.cs b/src/IIIFPresentation/API/Features/Storage/StorageController.cs index 252d2a121..3b3cc2367 100644 --- a/src/IIIFPresentation/API/Features/Storage/StorageController.cs +++ b/src/IIIFPresentation/API/Features/Storage/StorageController.cs @@ -9,7 +9,7 @@ using API.Infrastructure.Http; using API.Infrastructure.Requests; using API.Settings; -using MediatR; +using Mediator; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.WebUtilities; diff --git a/src/IIIFPresentation/API/Infrastructure/PresentationController.cs b/src/IIIFPresentation/API/Infrastructure/PresentationController.cs index c25bbf8ea..27742e677 100644 --- a/src/IIIFPresentation/API/Infrastructure/PresentationController.cs +++ b/src/IIIFPresentation/API/Infrastructure/PresentationController.cs @@ -4,7 +4,7 @@ using API.Settings; using Core; using IIIF; -using MediatR; +using Mediator; using Microsoft.AspNetCore.Mvc; namespace API.Infrastructure; @@ -72,7 +72,7 @@ protected async Task HandleUpsert( /// /// Handles a deletion /// - /// The request/response to be sent through Mediatr + /// The request/response to be sent through Mediator /// The title of the error /// Current cancellation token /// Thrown when the is not understood @@ -98,7 +98,7 @@ protected async Task HandleDelete( /// /// Handles a deletion, turning DeleteResult to a http response /// - /// The request/response to be sent through Mediatr + /// The request/response to be sent through Mediator /// The title of the error /// Current cancellation token /// Thrown when the is not understood diff --git a/src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/CacheInvalidationBehaviour.cs b/src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/CacheInvalidationBehaviour.cs index d7907211e..03f6862d5 100644 --- a/src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/CacheInvalidationBehaviour.cs +++ b/src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/CacheInvalidationBehaviour.cs @@ -1,10 +1,10 @@ using LazyCache; -using MediatR; +using Mediator; namespace API.Infrastructure.Requests.Pipelines; /// -/// Interface for Mediatr requests that invalidate cache records on success +/// Interface for Mediator requests that invalidate cache records on success /// public interface IInvalidateCaches { @@ -15,7 +15,7 @@ public interface IInvalidateCaches } /// -/// MediatR behaviour that will clear cacheKeys specified in request if request was successful +/// Mediator behaviour that will clear cacheKeys specified in request if request was successful /// public class CacheInvalidationBehaviour : IPipelineBehavior where TRequest : IInvalidateCaches, IRequest @@ -31,10 +31,10 @@ public CacheInvalidationBehaviour(IAppCache appCache, this.logger = logger; } - public async Task Handle(TRequest request, RequestHandlerDelegate next, + public async ValueTask Handle(TRequest request, MessageHandlerDelegate next, CancellationToken cancellationToken) { - var nextResponse = await next(); + var nextResponse = await next(request, cancellationToken); if (nextResponse.IsSuccess) InvalidateCacheKeys(request); diff --git a/src/IIIFPresentation/API/Infrastructure/Mediatr/Behaviours/LoggingBehaviour.cs b/src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/LoggingBehaviour.cs similarity index 74% rename from src/IIIFPresentation/API/Infrastructure/Mediatr/Behaviours/LoggingBehaviour.cs rename to src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/LoggingBehaviour.cs index 456be17dd..ad8f27c9d 100644 --- a/src/IIIFPresentation/API/Infrastructure/Mediatr/Behaviours/LoggingBehaviour.cs +++ b/src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/LoggingBehaviour.cs @@ -1,10 +1,10 @@ using System.Diagnostics; -using MediatR; +using Mediator; -namespace API.Infrastructure.Mediatr.Behaviours; +namespace API.Infrastructure.Requests.Pipelines; /// -/// Mediatr Pipeline Behaviour that logs requests with timings. +/// Mediator pipeline behaviour that logs requests with timings. /// Will use ToString() property to log details /// public class LoggingBehavior : IPipelineBehavior @@ -17,14 +17,14 @@ public LoggingBehavior(ILogger> logger) this.logger = logger; } - public async Task Handle(TRequest request, RequestHandlerDelegate next, + public async ValueTask Handle(TRequest request, MessageHandlerDelegate next, CancellationToken cancellationToken) { // This could be cleverer, currently will just log ToString() logger.LogTrace("Handling '{RequestType}' request. {Request}", typeof(TRequest).Name, request); var sw = Stopwatch.StartNew(); - var response = await next(); + var response = await next(request, cancellationToken); sw.Stop(); logger.LogTrace("Handled '{RequestType}' in {Elapsed}ms. {Request}", typeof(TRequest).Name, diff --git a/src/IIIFPresentation/API/Infrastructure/ServiceCollectionX.cs b/src/IIIFPresentation/API/Infrastructure/ServiceCollectionX.cs index 371049ac4..62129f7fc 100644 --- a/src/IIIFPresentation/API/Infrastructure/ServiceCollectionX.cs +++ b/src/IIIFPresentation/API/Infrastructure/ServiceCollectionX.cs @@ -1,12 +1,10 @@ -using System.Reflection; -using API.Infrastructure.IdGenerator; -using API.Infrastructure.Mediatr.Behaviours; +using API.Infrastructure.IdGenerator; using API.Infrastructure.Requests.Pipelines; using API.Settings; using AWS.Configuration; using AWS.Helpers; using AWS.S3; -using MediatR; +using Mediator; using Microsoft.OpenApi; using Repository; using Sqids; @@ -36,14 +34,18 @@ public static IServiceCollection AddCaching(this IServiceCollection services, Ca .AddLazyCache(); /// - /// Add MediatR services and pipeline behaviours to service collection. + /// Add Mediator services and pipeline behaviours to service collection. /// - public static IServiceCollection ConfigureMediatR(this IServiceCollection services) + public static IServiceCollection ConfigureMediator(this IServiceCollection services) { return services - .AddMediatR(config => config.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())) - .AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)) - .AddScoped(typeof(IPipelineBehavior<,>), typeof(CacheInvalidationBehaviour<,>)); + .AddMediator(options => + { + // Handlers depend on scoped services (PresentationContext etc); Mediator defaults to + // Singleton, which would capture those as long-lived dependencies. + options.ServiceLifetime = ServiceLifetime.Scoped; + options.PipelineBehaviors = [typeof(LoggingBehavior<,>), typeof(CacheInvalidationBehaviour<,>)]; + }); } /// diff --git a/src/IIIFPresentation/API/Program.cs b/src/IIIFPresentation/API/Program.cs index 0d1974778..9611bb612 100644 --- a/src/IIIFPresentation/API/Program.cs +++ b/src/IIIFPresentation/API/Program.cs @@ -101,7 +101,7 @@ .AddHttpContextAccessor() .AddScoped() .AddOutgoingHeaders(); -builder.Services.ConfigureMediatR(); +builder.Services.ConfigureMediator(); builder.Services.ConfigureIdGenerator(); builder.Services .AddHealthChecks() diff --git a/src/IIIFPresentation/Repository/Repository.csproj b/src/IIIFPresentation/Repository/Repository.csproj index 3883eae0c..ef25b767b 100644 --- a/src/IIIFPresentation/Repository/Repository.csproj +++ b/src/IIIFPresentation/Repository/Repository.csproj @@ -11,7 +11,6 @@ - From bf5b1e7598d238f4ed3b29e4b0c4bb5e1d21b50a Mon Sep 17 00:00:00 2001 From: Donald Gray Date: Tue, 18 Aug 2026 15:58:37 +0100 Subject: [PATCH 3/7] Some tidy/refactor after dotnet10 upgrade * Remove unused method/classes * Remove unused using * Resolve some unused usings * Introduce some extension blocks to tidy helper classes * Remove obsolete props --- .../Manifest/CanvasPaintingResolver.cs | 4 +- .../Features/Manifest/ManifestReadService.cs | 2 +- .../Features/Manifest/ManifestWriteService.cs | 4 +- .../Storage/CollectionWriteService.cs | 2 +- .../Helpers/HierarchicalCollectionResponse.cs | 3 +- .../Storage/Requests/DeleteCollection.cs | 2 - .../API/Helpers/CollectionHelperX.cs | 3 +- .../Helpers/HttpRequestBasedPathGenerator.cs | 4 +- .../API/Helpers/ParentSlugParser.cs | 1 + .../API/Helpers/PresentationX.cs | 3 +- .../API/Infrastructure/ControllerBaseX.cs | 11 +- .../API/Infrastructure/ETagCache.cs | 1 - .../Infrastructure/Helpers/HttpRequestX.cs | 71 +++--- .../Infrastructure/PresentationController.cs | 33 +-- .../Requests/DeleteEntityResult.cs | 30 --- .../Requests/ModifyEntityResult.cs | 13 -- .../Pipelines/CacheInvalidationBehaviour.cs | 17 +- .../Requests/Pipelines/LoggingBehaviour.cs | 10 +- .../Requests/PresentationResult.cs | 4 +- .../API/Infrastructure/ServiceCollectionX.cs | 202 +++++++++--------- ...HostnameDrivenPresentationPathGenerator.cs | 3 +- src/IIIFPresentation/API/Program.cs | 2 +- .../Infrastructure/ServiceCollectionX.cs | 91 ++++---- 23 files changed, 218 insertions(+), 298 deletions(-) delete mode 100644 src/IIIFPresentation/API/Infrastructure/Requests/DeleteEntityResult.cs diff --git a/src/IIIFPresentation/API/Features/Manifest/CanvasPaintingResolver.cs b/src/IIIFPresentation/API/Features/Manifest/CanvasPaintingResolver.cs index e5567ec0a..10c4dddcd 100644 --- a/src/IIIFPresentation/API/Features/Manifest/CanvasPaintingResolver.cs +++ b/src/IIIFPresentation/API/Features/Manifest/CanvasPaintingResolver.cs @@ -37,7 +37,7 @@ public async Task GenerateCanvasPaintings( var manifestParseResult = await ParseManifest(customerId, presentationManifest); if (manifestParseResult.Error != null) return ParsedManifestResult.Failure(manifestParseResult.Error); - Debug.Assert(manifestParseResult.CanvasPaintings is not null, "manifestParseResult.CanvasPaintings is not null"); + Debug.Assert(manifestParseResult.CanvasPaintings is not null); var insertCanvasPaintingsError = await HandleInserts(manifestParseResult.CanvasPaintings, customerId, cancellationToken); if (insertCanvasPaintingsError != null) return ParsedManifestResult.Failure(insertCanvasPaintingsError); @@ -71,7 +71,7 @@ public async Task UpdateCanvasPaintings(int customerId, Pr if (manifestParseResult.Error != null) return ParsedManifestResult.Failure(manifestParseResult.Error); existingManifest.CanvasPaintings ??= []; - Debug.Assert(manifestParseResult.CanvasPaintings is not null, "manifestParseResult.CanvasPaintings is not null"); + Debug.Assert(manifestParseResult.CanvasPaintings is not null); var toInsert = UpdateCanvasPaintingRecords(existingManifest.CanvasPaintings, manifestParseResult.CanvasPaintings, existingManifest.SpaceId); diff --git a/src/IIIFPresentation/API/Features/Manifest/ManifestReadService.cs b/src/IIIFPresentation/API/Features/Manifest/ManifestReadService.cs index 4948175be..ba66b1a83 100644 --- a/src/IIIFPresentation/API/Features/Manifest/ManifestReadService.cs +++ b/src/IIIFPresentation/API/Features/Manifest/ManifestReadService.cs @@ -71,7 +71,7 @@ public async Task> GetManifest(int custo // or if not found in "staging", an error was logged and we fall back to "real" manifest ??= await iiifS3.ReadIIIFFromS3(dbManifest, BucketLocationType.Default, cancellationToken); - dbManifest.Hierarchy.Single().FullPath = await fetchFullPath; + dbManifest.Hierarchy!.Single().FullPath = await fetchFullPath; if (manifest == null) return FetchEntityResult.Failure( diff --git a/src/IIIFPresentation/API/Features/Manifest/ManifestWriteService.cs b/src/IIIFPresentation/API/Features/Manifest/ManifestWriteService.cs index 815d6a652..e0887d78f 100644 --- a/src/IIIFPresentation/API/Features/Manifest/ManifestWriteService.cs +++ b/src/IIIFPresentation/API/Features/Manifest/ManifestWriteService.cs @@ -351,7 +351,7 @@ private async Task GeneratePresentationSuccessResult(Present presentationManifest.SetGeneratedFields(dbManifest, pathGenerator, savedManifestPathGenerator, assets, finishedPipelinesLimit: options.Value.FinishedPipelinesLimit), writeResult, - dbManifest?.Etag); + dbManifest.Etag); } private async Task<(PresentationResult?, DbManifest?)> CreateDatabaseRecord(WriteManifestRequest request, @@ -414,7 +414,7 @@ private async Task GeneratePresentationSuccessResult(Present if (saveErrors != null) return saveErrors; - dbManifest.Hierarchy.Single().FullPath = + dbManifest.Hierarchy!.Single().FullPath = await ManifestRetrieval.RetrieveFullPathForManifest(dbManifest.Id, dbManifest.CustomerId, dbContext, cancellationToken); return null; diff --git a/src/IIIFPresentation/API/Features/Storage/CollectionWriteService.cs b/src/IIIFPresentation/API/Features/Storage/CollectionWriteService.cs index 65988bc67..eb20011be 100644 --- a/src/IIIFPresentation/API/Features/Storage/CollectionWriteService.cs +++ b/src/IIIFPresentation/API/Features/Storage/CollectionWriteService.cs @@ -212,7 +212,7 @@ private async Task UpdateInternal(UpsertCollectionRequest re var saveErrors = await dbContext.TrySave("collection", request.CustomerId, logger, cancellationToken); if (saveErrors != null) return saveErrors; - var hierarchy = databaseCollection.Hierarchy.Single(); + var hierarchy = databaseCollection.Hierarchy!.Single(); if (hierarchy.Parent != null) { var fullPathError = await TrySetFullPath(databaseCollection, hierarchy, cancellationToken); diff --git a/src/IIIFPresentation/API/Features/Storage/Helpers/HierarchicalCollectionResponse.cs b/src/IIIFPresentation/API/Features/Storage/Helpers/HierarchicalCollectionResponse.cs index dd88dce0c..a3d0414e2 100644 --- a/src/IIIFPresentation/API/Features/Storage/Helpers/HierarchicalCollectionResponse.cs +++ b/src/IIIFPresentation/API/Features/Storage/Helpers/HierarchicalCollectionResponse.cs @@ -3,6 +3,7 @@ using Core.Helpers; using IIIF.Presentation; using Models.API.Collection; +using Services.Manifests.Helpers; namespace API.Features.Storage.Helpers; @@ -17,7 +18,7 @@ public static class HierarchicalCollectionResponse /// collections - preserving any custom behaviors that the write service's enriched entity would otherwise have /// discarded (see ). The response /// id is taken from the enriched entity's PublicId, which already accounts for customers with a - /// configured path. + /// configured path. /// /// Result of the underlying call /// The raw request body, re-parsed for non-storage collections diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/DeleteCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/DeleteCollection.cs index 499be110b..755215221 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/DeleteCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/DeleteCollection.cs @@ -1,11 +1,9 @@ using API.Features.Common.Helpers; -using API.Features.Storage.Helpers; using Core; using Mediator; using Models; using Models.API.General; using Models.Database.Collections; -using Repository; namespace API.Features.Storage.Requests; diff --git a/src/IIIFPresentation/API/Helpers/CollectionHelperX.cs b/src/IIIFPresentation/API/Helpers/CollectionHelperX.cs index 2880fef07..addf130f7 100644 --- a/src/IIIFPresentation/API/Helpers/CollectionHelperX.cs +++ b/src/IIIFPresentation/API/Helpers/CollectionHelperX.cs @@ -1,5 +1,4 @@ -using API.Infrastructure.Validation; -using Models.API.General; +using Models.API.General; using Models.Database.Collections; namespace API.Helpers; diff --git a/src/IIIFPresentation/API/Helpers/HttpRequestBasedPathGenerator.cs b/src/IIIFPresentation/API/Helpers/HttpRequestBasedPathGenerator.cs index c2fef19cf..968cc4cca 100644 --- a/src/IIIFPresentation/API/Helpers/HttpRequestBasedPathGenerator.cs +++ b/src/IIIFPresentation/API/Helpers/HttpRequestBasedPathGenerator.cs @@ -1,6 +1,4 @@ -using API.Infrastructure.Requests; -using Core.Web; -using DLCS; +using DLCS; using Microsoft.Extensions.Options; using Repository.Paths; diff --git a/src/IIIFPresentation/API/Helpers/ParentSlugParser.cs b/src/IIIFPresentation/API/Helpers/ParentSlugParser.cs index 106188c85..811d55578 100644 --- a/src/IIIFPresentation/API/Helpers/ParentSlugParser.cs +++ b/src/IIIFPresentation/API/Helpers/ParentSlugParser.cs @@ -35,6 +35,7 @@ public interface IParentSlugParser /// Slug for the resource - for hierarchical PUT, the last segment of the path; for flat requests, derived from /// the body's "id" property when it resolves to an own-host hierarchical id /// + /// Current cancellation token public Task Parse( T presentation, int customerId, diff --git a/src/IIIFPresentation/API/Helpers/PresentationX.cs b/src/IIIFPresentation/API/Helpers/PresentationX.cs index 4158a8b7d..2d91b3dad 100644 --- a/src/IIIFPresentation/API/Helpers/PresentationX.cs +++ b/src/IIIFPresentation/API/Helpers/PresentationX.cs @@ -1,5 +1,4 @@ -using API.Infrastructure.Validation; -using Core.Helpers; +using Core.Helpers; using Models.API; using Models.API.General; diff --git a/src/IIIFPresentation/API/Infrastructure/ControllerBaseX.cs b/src/IIIFPresentation/API/Infrastructure/ControllerBaseX.cs index 34326ce13..e7c4a7d0a 100644 --- a/src/IIIFPresentation/API/Infrastructure/ControllerBaseX.cs +++ b/src/IIIFPresentation/API/Infrastructure/ControllerBaseX.cs @@ -1,5 +1,4 @@ using System.Net; -using System.Runtime.InteropServices.JavaScript; using API.Features.Storage.Helpers; using API.Infrastructure.Http; using API.Infrastructure.Requests; @@ -63,9 +62,9 @@ public static IActionResult FetchResultToHttpResult(this ControllerBase contr /// /// Current controllerBase object /// Result to transform - /// The value for . + /// The value for . /// - /// The value for . In some instances this will be prepended to the actual error name. + /// The value for . In some instances this will be prepended to the actual error name. /// e.g. errorTitle + ": Conflict" /// /// @@ -77,11 +76,11 @@ public static IActionResult ModifyResultToHttpResult(this ControllerBase control string? errorTitle) => entityResult.WriteResult switch { - WriteResult.Updated => controller.PresentationContent(entityResult.Entity, etag: entityResult.ETag), + WriteResult.Updated => controller.PresentationContent(entityResult.Entity!, etag: entityResult.ETag), WriteResult.Accepted => controller.PresentationWithLocationHeader(controller.Request.GetDisplayUrl(), - entityResult.Entity, (int)HttpStatusCode.Accepted, null), + entityResult.Entity!, (int)HttpStatusCode.Accepted, null), WriteResult.Created => controller.PresentationWithLocationHeader(controller.Request.GetDisplayUrl(), - entityResult.Entity, (int)HttpStatusCode.Created, entityResult.ETag), + entityResult.Entity!, (int)HttpStatusCode.Created, entityResult.ETag), WriteResult.NotFound => controller.PresentationNotFound(entityResult.Error), WriteResult.Error => controller.PresentationProblem(entityResult.Error, instance, (int)HttpStatusCode.InternalServerError, errorTitle, controller.GetErrorType(entityResult.ErrorType)), diff --git a/src/IIIFPresentation/API/Infrastructure/ETagCache.cs b/src/IIIFPresentation/API/Infrastructure/ETagCache.cs index 42d51724b..c2e2dbef0 100644 --- a/src/IIIFPresentation/API/Infrastructure/ETagCache.cs +++ b/src/IIIFPresentation/API/Infrastructure/ETagCache.cs @@ -1,6 +1,5 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; -using API.Features.Manifest.Requests; using API.Settings; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; diff --git a/src/IIIFPresentation/API/Infrastructure/Helpers/HttpRequestX.cs b/src/IIIFPresentation/API/Infrastructure/Helpers/HttpRequestX.cs index df3ed07fc..5ffd74acf 100644 --- a/src/IIIFPresentation/API/Infrastructure/Helpers/HttpRequestX.cs +++ b/src/IIIFPresentation/API/Infrastructure/Helpers/HttpRequestX.cs @@ -7,45 +7,48 @@ public static class HttpRequestX private static readonly KeyValuePair AdditionalPropertiesHeader = new (CustomHttpHeaders.ShowExtras, "All"); private const string CreateSpaceHeader = ";rel=\"DCTERMS.requires\""; - /// - /// Checks if the has appropriate header to show additional parameters - /// - public static bool HasShowExtraHeader(this HttpRequest request) - { - return request.Headers.FirstOrDefault(h => string.Equals(h.Key, AdditionalPropertiesHeader.Key, StringComparison.OrdinalIgnoreCase)).Value == - AdditionalPropertiesHeader.Value; - } - - /// - /// Checks if the has header requesting a space be created - /// - public static bool HasCreateSpaceHeader(this HttpRequest request) - => request.Headers.Link.Contains(CreateSpaceHeader); - - /// - /// Retrieve the customer id - /// - /// NOTE: retrieved from route values - /// /// The request to get the customer id from - /// A parsed customer id - public static int? GetCustomerId(this HttpRequest request, ILogger logger) + extension(HttpRequest request) { - var customerIdRouteValue = "customerId"; - - if (!request.RouteValues.TryGetValue(customerIdRouteValue, out var customerIdRouteVal) - || customerIdRouteVal is null) + /// + /// Checks if the has appropriate header to show additional parameters + /// + public bool HasShowExtraHeader() { - logger.LogDebug("Unable to identify customerId in auth request to {Path}", request.Path); - return null; + return request.Headers.FirstOrDefault(h => string.Equals(h.Key, AdditionalPropertiesHeader.Key, StringComparison.OrdinalIgnoreCase)).Value == + AdditionalPropertiesHeader.Value; } - - if (!int.TryParse(customerIdRouteVal.ToString(), out int customerId)) + + /// + /// Checks if the has header requesting a space be created + /// + public bool HasCreateSpaceHeader() + => request.Headers.Link.Contains(CreateSpaceHeader); + + /// + /// Retrieve the customer id + /// + /// NOTE: retrieved from route values + /// + /// A parsed customer id + public int? GetCustomerId(ILogger logger) { - logger.LogDebug("Specified customerId is not numeric {Path}", request.Path); - return null; - } + const string customerIdRouteValue = "customerId"; - return customerId; + if (!request.RouteValues.TryGetValue(customerIdRouteValue, out var customerIdRouteVal) + || customerIdRouteVal is null) + { + logger.LogDebug("Unable to identify customerId in auth request to {Path}", request.Path); + return null; + } + + if (!int.TryParse(customerIdRouteVal.ToString(), out int customerId)) + { + logger.LogDebug("Specified customerId is not numeric {Path}", request.Path); + return null; + } + + return customerId; + } } } diff --git a/src/IIIFPresentation/API/Infrastructure/PresentationController.cs b/src/IIIFPresentation/API/Infrastructure/PresentationController.cs index 27742e677..2f23269ca 100644 --- a/src/IIIFPresentation/API/Infrastructure/PresentationController.cs +++ b/src/IIIFPresentation/API/Infrastructure/PresentationController.cs @@ -6,6 +6,7 @@ using IIIF; using Mediator; using Microsoft.AspNetCore.Mvc; +using Models.API.General; namespace API.Infrastructure; @@ -41,9 +42,9 @@ protected PresentationController(ApiSettings settings, IMediator mediator, IETag /// The request is sent and result is transformed to an http result. /// /// IRequest to modify data - /// The value for . + /// The value for . /// - /// The value for . In some instances this will be prepended to the actual error name. + /// The value for . In some instances this will be prepended to the actual error name. /// e.g. errorTitle + ": Conflict" /// /// string etag value used in this request, optional @@ -95,30 +96,6 @@ protected async Task HandleDelete( }, errorTitle); } - /// - /// Handles a deletion, turning DeleteResult to a http response - /// - /// The request/response to be sent through Mediator - /// The title of the error - /// Current cancellation token - /// Thrown when the is not understood - /// - /// ActionResult generated from DeleteResult. This will be 204 on success. Or an - /// error and appropriate status code if failed. - /// - protected async Task HandleDelete( - IRequest request, - string? errorTitle = "Delete failed", - CancellationToken cancellationToken = default) - { - return await HandleRequest(async () => - { - var result = await Mediator.Send(request, cancellationToken); - - return ConvertDeleteToHttp(result.Value, result.Message, result.Type); - }, errorTitle); - } - private IActionResult ConvertDeleteToHttp(DeleteResult result, string? message, TType type) { return result switch @@ -140,9 +117,9 @@ private IActionResult ConvertDeleteToHttp(DeleteResult result, string? me /// The request is sent and result is transformed to an http result. /// /// IRequest to fetch data - /// The value for . + /// The value for . /// - /// The value for . In some instances this will be prepended to the actual error name. + /// The value for . In some instances this will be prepended to the actual error name. /// e.g. errorTitle + ": Conflict" /// /// Current cancellation token diff --git a/src/IIIFPresentation/API/Infrastructure/Requests/DeleteEntityResult.cs b/src/IIIFPresentation/API/Infrastructure/Requests/DeleteEntityResult.cs deleted file mode 100644 index b905ee7c2..000000000 --- a/src/IIIFPresentation/API/Infrastructure/Requests/DeleteEntityResult.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Core; - -namespace API.Infrastructure.Requests; - -/// -/// Represents the result of a request to delete an entity -/// -public class DeleteEntityResult : IModifyRequest -{ - /// - /// The associated value. - /// - public DeleteResult Value { get; private init; } - - /// - /// The message related to the result - /// - public string? Message { get; private init; } - - public string? Type { get; private init; } - - public static DeleteEntityResult Success => new() { Value = DeleteResult.Deleted }; - - public bool IsSuccess => Value == DeleteResult.Deleted; - - public static DeleteEntityResult Failure(string message, DeleteResult result, string type) - { - return new DeleteEntityResult { Message = message, Value = result, Type = type}; - } -} \ No newline at end of file diff --git a/src/IIIFPresentation/API/Infrastructure/Requests/ModifyEntityResult.cs b/src/IIIFPresentation/API/Infrastructure/Requests/ModifyEntityResult.cs index 21f115f3f..3d354da86 100644 --- a/src/IIIFPresentation/API/Infrastructure/Requests/ModifyEntityResult.cs +++ b/src/IIIFPresentation/API/Infrastructure/Requests/ModifyEntityResult.cs @@ -36,17 +36,4 @@ public class ModifyEntityResult : IModifyRequest public TError? ErrorType { get; protected init; } public Guid? ETag { get; protected init; } - - public static ModifyEntityResult Failure(string error, TError errorType, WriteResult result = WriteResult.Unknown) - { - return new ModifyEntityResult - { Error = error, WriteResult = result, IsSuccess = false, ErrorType = errorType }; - } - - public static ModifyEntityResult Success(JsonLdBase entity, WriteResult result = WriteResult.Updated, Guid? etag = null) - { - return new ModifyEntityResult - { Entity = entity, WriteResult = result, IsSuccess = true, ETag = etag }; - } - } diff --git a/src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/CacheInvalidationBehaviour.cs b/src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/CacheInvalidationBehaviour.cs index 03f6862d5..4775b816c 100644 --- a/src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/CacheInvalidationBehaviour.cs +++ b/src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/CacheInvalidationBehaviour.cs @@ -17,20 +17,13 @@ public interface IInvalidateCaches /// /// Mediator behaviour that will clear cacheKeys specified in request if request was successful /// -public class CacheInvalidationBehaviour : IPipelineBehavior +public class CacheInvalidationBehaviour( + IAppCache appCache, + ILogger> logger) + : IPipelineBehavior where TRequest : IInvalidateCaches, IRequest where TResponse : IModifyRequest { - private readonly IAppCache appCache; - private readonly ILogger> logger; - - public CacheInvalidationBehaviour(IAppCache appCache, - ILogger> logger) - { - this.appCache = appCache; - this.logger = logger; - } - public async ValueTask Handle(TRequest request, MessageHandlerDelegate next, CancellationToken cancellationToken) { @@ -49,4 +42,4 @@ private void InvalidateCacheKeys(IInvalidateCaches request) appCache.Remove(cacheKey); } } -} \ No newline at end of file +} diff --git a/src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/LoggingBehaviour.cs b/src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/LoggingBehaviour.cs index ad8f27c9d..8fcd57eaa 100644 --- a/src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/LoggingBehaviour.cs +++ b/src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/LoggingBehaviour.cs @@ -7,16 +7,10 @@ namespace API.Infrastructure.Requests.Pipelines; /// Mediator pipeline behaviour that logs requests with timings. /// Will use ToString() property to log details /// -public class LoggingBehavior : IPipelineBehavior +public class LoggingBehavior(ILogger> logger) + : IPipelineBehavior where TRequest : IRequest, IBaseRequest { - private readonly ILogger> logger; - - public LoggingBehavior(ILogger> logger) - { - this.logger = logger; - } - public async ValueTask Handle(TRequest request, MessageHandlerDelegate next, CancellationToken cancellationToken) { diff --git a/src/IIIFPresentation/API/Infrastructure/Requests/PresentationResult.cs b/src/IIIFPresentation/API/Infrastructure/Requests/PresentationResult.cs index 3ff9ccc05..282eaff88 100644 --- a/src/IIIFPresentation/API/Infrastructure/Requests/PresentationResult.cs +++ b/src/IIIFPresentation/API/Infrastructure/Requests/PresentationResult.cs @@ -10,11 +10,11 @@ namespace API.Infrastructure.Requests; /// public class PresentationResult : ModifyEntityResult { - public new static PresentationResult Failure(string error, ModifyCollectionType errorType, + public static PresentationResult Failure(string error, ModifyCollectionType errorType, WriteResult result = WriteResult.Unknown) => new() { Error = error, WriteResult = result, IsSuccess = false, ErrorType = errorType }; - public new static PresentationResult Success(JsonLdBase entity, WriteResult result = WriteResult.Updated, + public static PresentationResult Success(JsonLdBase entity, WriteResult result = WriteResult.Updated, Guid? etag = null) => new() { Entity = entity, WriteResult = result, IsSuccess = true, ETag = etag }; } diff --git a/src/IIIFPresentation/API/Infrastructure/ServiceCollectionX.cs b/src/IIIFPresentation/API/Infrastructure/ServiceCollectionX.cs index 62129f7fc..44e5507a8 100644 --- a/src/IIIFPresentation/API/Infrastructure/ServiceCollectionX.cs +++ b/src/IIIFPresentation/API/Infrastructure/ServiceCollectionX.cs @@ -13,115 +13,117 @@ namespace API.Infrastructure; public static class ServiceCollectionX { - /// - /// Add all dataaccess dependencies, including repositories and presentation context - /// - public static IServiceCollection AddDataAccess(this IServiceCollection services, IConfiguration configuration) - { - return services - .AddPresentationContext(configuration); - } - - /// - /// Configure caching - /// - public static IServiceCollection AddCaching(this IServiceCollection services, CacheSettings cacheSettings) - => services.AddMemoryCache(memoryCacheOptions => - { - memoryCacheOptions.SizeLimit = cacheSettings.MemoryCacheSizeLimit; - memoryCacheOptions.CompactionPercentage = cacheSettings.MemoryCacheCompactionPercentage; - }) - .AddLazyCache(); - - /// - /// Add Mediator services and pipeline behaviours to service collection. - /// - public static IServiceCollection ConfigureMediator(this IServiceCollection services) + /// Current object + extension(IServiceCollection services) { - return services - .AddMediator(options => - { - // Handlers depend on scoped services (PresentationContext etc); Mediator defaults to - // Singleton, which would capture those as long-lived dependencies. - options.ServiceLifetime = ServiceLifetime.Scoped; - options.PipelineBehaviors = [typeof(LoggingBehavior<,>), typeof(CacheInvalidationBehaviour<,>)]; - }); - } + /// + /// Add all dataaccess dependencies, including repositories and presentation context + /// + public IServiceCollection AddDataAccess(IConfiguration configuration) + { + return services + .AddPresentationContext(configuration); + } - /// - /// Add services for identity generation - /// - public static IServiceCollection ConfigureIdGenerator(this IServiceCollection services) - { - return services.AddSingleton(new SqidsEncoder(new() - { - Alphabet = "abcdefghijklmnopqrstuvwxyz0123456789", - MinLength = 6, - })) - .AddSingleton() - .AddScoped(); - } - - /// - /// Add required AWS services - /// - public static IServiceCollection AddAws(this IServiceCollection services, - IConfiguration configuration, IWebHostEnvironment webHostEnvironment) - { - services - .AddSingleton() - .AddSingleton() - .AddSingleton(); + /// + /// Configure caching + /// + public IServiceCollection AddCaching(CacheSettings cacheSettings) + => services.AddMemoryCache(memoryCacheOptions => + { + memoryCacheOptions.SizeLimit = cacheSettings.MemoryCacheSizeLimit; + memoryCacheOptions.CompactionPercentage = cacheSettings.MemoryCacheCompactionPercentage; + }) + .AddLazyCache(); - services - .SetupAWS(configuration, webHostEnvironment) - .WithAmazonS3(); + /// + /// Add Mediator services and pipeline behaviours to service collection. + /// + public IServiceCollection ConfigureMediator() + { + return services + .AddMediator(options => + { + // Handlers depend on scoped services (PresentationContext etc); Mediator defaults to + // Singleton, which would capture those as long-lived dependencies. + options.ServiceLifetime = ServiceLifetime.Scoped; + options.PipelineBehaviors = [typeof(LoggingBehavior<,>), typeof(CacheInvalidationBehaviour<,>)]; + }); + } - return services; - } - - /// - /// Add Cors policy allowing any Origin, Method and Header - /// - /// Current object - /// Cors policy name - public static IServiceCollection ConfigureDefaultCors(this IServiceCollection services, string policyName) - => services.AddCors(options => + /// + /// Add services for identity generation + /// + public IServiceCollection ConfigureIdGenerator() { - options.AddPolicy(policyName, builder => builder - .AllowAnyOrigin() - .AllowAnyMethod() - .AllowAnyHeader()); - }); - - /// - /// Add SwaggerGen services to service collection. - /// - public static IServiceCollection ConfigureSwagger(this IServiceCollection services) - => services - .AddEndpointsApiExplorer() - .AddSwaggerGen(c => + return services.AddSingleton(new SqidsEncoder(new() + { + Alphabet = "abcdefghijklmnopqrstuvwxyz0123456789", + MinLength = 6, + })) + .AddSingleton() + .AddScoped(); + } + + /// + /// Add required AWS services + /// + public IServiceCollection AddAws(IConfiguration configuration, IWebHostEnvironment webHostEnvironment) { - c.SwaggerDoc("v1", new OpenApiInfo + services + .AddSingleton() + .AddSingleton() + .AddSingleton(); + + services + .SetupAWS(configuration, webHostEnvironment) + .WithAmazonS3(); + + return services; + } + + /// + /// Add Cors policy allowing any Origin, Method and Header + /// + /// Cors policy name + public IServiceCollection ConfigureDefaultCors(string policyName) + => services.AddCors(options => { - Title = "IIIF Presentation API", - Version = "v1", - Description = "API for creation and management of IIIF Presentation API resources" + options.AddPolicy(policyName, builder => builder + .AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader()); }); - c.AddSecurityDefinition( - "basic", new OpenApiSecurityScheme + /// + /// Add SwaggerGen services to service collection. + /// + public IServiceCollection ConfigureSwagger() + => services + .AddEndpointsApiExplorer() + .AddSwaggerGen(c => { - Name = "Authorization", - Type = SecuritySchemeType.Http, - Scheme = "basic", - In = ParameterLocation.Header, - Description = "Basic Authorization header", - }); + c.SwaggerDoc("v1", new OpenApiInfo + { + Title = "IIIF Presentation API", + Version = "v1", + Description = "API for creation and management of IIIF Presentation API resources" + }); - c.AddSecurityRequirement((document) => new OpenApiSecurityRequirement - { - [new OpenApiSecuritySchemeReference("basic", document)] = [] - }); - }); + c.AddSecurityDefinition( + "basic", new OpenApiSecurityScheme + { + Name = "Authorization", + Type = SecuritySchemeType.Http, + Scheme = "basic", + In = ParameterLocation.Header, + Description = "Basic Authorization header", + }); + + c.AddSecurityRequirement((document) => new OpenApiSecurityRequirement + { + [new OpenApiSecuritySchemeReference("basic", document)] = [] + }); + }); + } } diff --git a/src/IIIFPresentation/API/Paths/HostnameDrivenPresentationPathGenerator.cs b/src/IIIFPresentation/API/Paths/HostnameDrivenPresentationPathGenerator.cs index 81b7b3c44..cf157804d 100644 --- a/src/IIIFPresentation/API/Paths/HostnameDrivenPresentationPathGenerator.cs +++ b/src/IIIFPresentation/API/Paths/HostnameDrivenPresentationPathGenerator.cs @@ -1,6 +1,5 @@ using API.Helpers; using API.Infrastructure.Requests; -using Core.Paths; using Core.Web; using Microsoft.Extensions.Options; using Repository.Paths; @@ -32,7 +31,7 @@ private string GetPresentationPath(string presentationServiceType, int customerI { var request = GetHttpRequest(); var host = request.Host.Value; - var template = settings.GetPathTemplateForHostAndType(host, presentationServiceType); + var template = settings.GetPathTemplateForHostAndType(host!, presentationServiceType); var path = template.GeneratePath(customerId, hierarchyPath, resourceId); diff --git a/src/IIIFPresentation/API/Program.cs b/src/IIIFPresentation/API/Program.cs index 9611bb612..15e1fa28b 100644 --- a/src/IIIFPresentation/API/Program.cs +++ b/src/IIIFPresentation/API/Program.cs @@ -112,7 +112,7 @@ opts.ForwardedHeaders = ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedProto; // https://github.com/dotnet/dotnet-docker/issues/6491 - opts.KnownNetworks.Clear(); + opts.KnownIPNetworks.Clear(); opts.KnownProxies.Clear(); }); diff --git a/src/IIIFPresentation/BackgroundHandler/Infrastructure/ServiceCollectionX.cs b/src/IIIFPresentation/BackgroundHandler/Infrastructure/ServiceCollectionX.cs index 4f4cd395d..2ad285497 100644 --- a/src/IIIFPresentation/BackgroundHandler/Infrastructure/ServiceCollectionX.cs +++ b/src/IIIFPresentation/BackgroundHandler/Infrastructure/ServiceCollectionX.cs @@ -8,65 +8,66 @@ using BackgroundHandler.Listener; using BackgroundHandler.TextCompletion; using Repository; -using Repository.Helpers; namespace BackgroundHandler.Infrastructure; public static class ServiceCollectionX { - public static IServiceCollection AddAws(this IServiceCollection services, - IConfiguration configuration, IWebHostEnvironment webHostEnvironment) + extension(IServiceCollection services) { - services - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(); - - services - .SetupAWS(configuration, webHostEnvironment) - .WithAmazonSQS() - .WithAmazonS3(); - - return services; - } - - public static IServiceCollection AddBackgroundServices(this IServiceCollection services, AWSSettings aws) - { - if (!string.IsNullOrEmpty(aws.SQS.CustomerCreatedQueueName)) + public IServiceCollection AddAws(IConfiguration configuration, IWebHostEnvironment webHostEnvironment) { services - .AddHostedService(sp => - ActivatorUtilities.CreateInstance>(sp, aws.SQS.CustomerCreatedQueueName)) - .AddScoped(); - } + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); - if (!string.IsNullOrEmpty(aws.SQS.BatchCompletionQueueName)) - { services - .AddHostedService(sp => - ActivatorUtilities.CreateInstance>(sp, aws.SQS.BatchCompletionQueueName)) - .AddScoped(); + .SetupAWS(configuration, webHostEnvironment) + .WithAmazonSQS() + .WithAmazonS3(); + + return services; } - if (!string.IsNullOrEmpty(aws.SQS.TextJobQueueName)) + public IServiceCollection AddBackgroundServices(AWSSettings aws) { - services - .AddHostedService(sp => - ActivatorUtilities.CreateInstance>(sp, aws.SQS.TextJobQueueName)) - .AddScoped(); + if (!string.IsNullOrEmpty(aws.SQS.CustomerCreatedQueueName)) + { + services + .AddHostedService(sp => + ActivatorUtilities.CreateInstance>(sp, aws.SQS.CustomerCreatedQueueName)) + .AddScoped(); + } + + if (!string.IsNullOrEmpty(aws.SQS.BatchCompletionQueueName)) + { + services + .AddHostedService(sp => + ActivatorUtilities.CreateInstance>(sp, aws.SQS.BatchCompletionQueueName)) + .AddScoped(); + } + + if (!string.IsNullOrEmpty(aws.SQS.TextJobQueueName)) + { + services + .AddHostedService(sp => + ActivatorUtilities.CreateInstance>(sp, aws.SQS.TextJobQueueName)) + .AddScoped(); + } + + return services; } - return services; - } - - /// - /// Add all dataaccess dependencies, including repositories and presentation context - /// - public static IServiceCollection AddDataAccess(this IServiceCollection services, IConfiguration configuration) - { - return services - .AddPresentationContext(configuration); + /// + /// Add all dataaccess dependencies, including repositories and presentation context + /// + public IServiceCollection AddDataAccess(IConfiguration configuration) + { + return services + .AddPresentationContext(configuration); + } } } From 4ce925dd478036eadf0fc579337fedb6472f2e34 Mon Sep 17 00:00:00 2001 From: Donald Gray Date: Wed, 19 Aug 2026 08:50:01 +0100 Subject: [PATCH 4/7] Revert "Switch from mediatr to mediator lib" This reverts commit 048384f50f61145a2ec91d214e15353ab1861e94. --- src/IIIFPresentation/API/API.csproj | 5 ----- .../Features/Manifest/ManifestController.cs | 2 +- .../Requests/CreateHierarchicalManifest.cs | 4 ++-- .../Manifest/Requests/CreateManifest.cs | 4 ++-- .../Manifest/Requests/DeleteManifest.cs | 4 ++-- .../Features/Manifest/Requests/GetManifest.cs | 8 ++++---- .../Requests/GetManifestHierarchical.cs | 4 ++-- .../Requests/UpsertHierarchicalManifest.cs | 4 ++-- .../Manifest/Requests/UpsertManifest.cs | 4 ++-- .../Features/Storage/CollectionController.cs | 2 +- .../Storage/Requests/CreateCollection.cs | 4 ++-- .../Requests/CreateHierarchicalCollection.cs | 4 ++-- .../Storage/Requests/DeleteCollection.cs | 4 ++-- .../Storage/Requests/GetCollection.cs | 4 ++-- .../Requests/GetHierarchicalCollection.cs | 4 ++-- .../Storage/Requests/SearchCollection.cs | 4 ++-- .../Storage/Requests/UpsertCollection.cs | 4 ++-- .../Requests/UpsertHierarchicalCollection.cs | 4 ++-- .../API/Features/Storage/StorageController.cs | 2 +- .../Infrastructure/PresentationController.cs | 4 ++-- .../Pipelines/CacheInvalidationBehaviour.cs | 12 ++++++------ .../Requests/Pipelines/LoggingBehaviour.cs | 12 ++++++------ .../API/Infrastructure/ServiceCollectionX.cs | 19 ++++++++----------- src/IIIFPresentation/API/Program.cs | 2 +- .../Repository/Repository.csproj | 1 + 25 files changed, 59 insertions(+), 66 deletions(-) diff --git a/src/IIIFPresentation/API/API.csproj b/src/IIIFPresentation/API/API.csproj index cff4b2e86..a353fbf2c 100644 --- a/src/IIIFPresentation/API/API.csproj +++ b/src/IIIFPresentation/API/API.csproj @@ -13,11 +13,6 @@ - - - all - runtime; build; native; contentfiles; analyzers - diff --git a/src/IIIFPresentation/API/Features/Manifest/ManifestController.cs b/src/IIIFPresentation/API/Features/Manifest/ManifestController.cs index 58bbb24a4..fb407e817 100644 --- a/src/IIIFPresentation/API/Features/Manifest/ManifestController.cs +++ b/src/IIIFPresentation/API/Features/Manifest/ManifestController.cs @@ -10,7 +10,7 @@ using API.Infrastructure.Requests; using API.Settings; using Models.API.General; -using Mediator; +using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; diff --git a/src/IIIFPresentation/API/Features/Manifest/Requests/CreateHierarchicalManifest.cs b/src/IIIFPresentation/API/Features/Manifest/Requests/CreateHierarchicalManifest.cs index 144d18734..2c3943baf 100644 --- a/src/IIIFPresentation/API/Features/Manifest/Requests/CreateHierarchicalManifest.cs +++ b/src/IIIFPresentation/API/Features/Manifest/Requests/CreateHierarchicalManifest.cs @@ -3,7 +3,7 @@ using API.Helpers; using API.Infrastructure.Requests; using API.Settings; -using Mediator; +using MediatR; using Microsoft.Extensions.Options; using Services.Manifests.Settings; @@ -37,7 +37,7 @@ public class CreateHierarchicalManifestHandler( IOptions apiOptions) : IRequestHandler { - public async ValueTask Handle(CreateHierarchicalManifest request, + public async Task Handle(CreateHierarchicalManifest request, CancellationToken cancellationToken) { var (error, context) = await hierarchicalRequestHelper.PrepareForCreate( diff --git a/src/IIIFPresentation/API/Features/Manifest/Requests/CreateManifest.cs b/src/IIIFPresentation/API/Features/Manifest/Requests/CreateManifest.cs index d46f7ecc5..70062d870 100644 --- a/src/IIIFPresentation/API/Features/Manifest/Requests/CreateManifest.cs +++ b/src/IIIFPresentation/API/Features/Manifest/Requests/CreateManifest.cs @@ -1,6 +1,6 @@ using API.Helpers; using API.Infrastructure.Requests; -using Mediator; +using MediatR; using Models.API.Manifest; namespace API.Features.Manifest.Requests; @@ -24,7 +24,7 @@ public class CreateManifest( public class CreateManifestHandler(IManifestWrite manifestService, IRequestIdResolver requestIdResolver) : IRequestHandler { - public async ValueTask Handle(CreateManifest request, CancellationToken cancellationToken) + public async Task Handle(CreateManifest request, CancellationToken cancellationToken) { var (error, resolvedId) = requestIdResolver.ResolveAndValidate(request.CustomerId, request.PresentationManifest.Id); diff --git a/src/IIIFPresentation/API/Features/Manifest/Requests/DeleteManifest.cs b/src/IIIFPresentation/API/Features/Manifest/Requests/DeleteManifest.cs index 409c8b2c3..7197eddcf 100644 --- a/src/IIIFPresentation/API/Features/Manifest/Requests/DeleteManifest.cs +++ b/src/IIIFPresentation/API/Features/Manifest/Requests/DeleteManifest.cs @@ -1,7 +1,7 @@ using API.Features.Common.Helpers; using API.Features.Storage.Helpers; using Core; -using Mediator; +using MediatR; using Models.API.General; using Repository; @@ -20,7 +20,7 @@ public class DeleteManifestHandler( ILogger logger) : IRequestHandler> { - public async ValueTask> Handle(DeleteManifest request, + public async Task> Handle(DeleteManifest request, CancellationToken cancellationToken) { logger.LogDebug("Deleting manifest {ManifestId} for customer {CustomerId}", request.ManifestId, diff --git a/src/IIIFPresentation/API/Features/Manifest/Requests/GetManifest.cs b/src/IIIFPresentation/API/Features/Manifest/Requests/GetManifest.cs index f0d8f7731..08ae8fde0 100644 --- a/src/IIIFPresentation/API/Features/Manifest/Requests/GetManifest.cs +++ b/src/IIIFPresentation/API/Features/Manifest/Requests/GetManifest.cs @@ -1,7 +1,7 @@ using System.Collections.Immutable; using API.Infrastructure.Helpers; using API.Infrastructure.Requests; -using Mediator; +using MediatR; using Microsoft.Extensions.Primitives; using Models.API.Manifest; @@ -26,8 +26,8 @@ public class GetManifest( public class GetManifestHandler(IManifestRead manifestRead) : IRequestHandler> { - public ValueTask> Handle(GetManifest request, + public Task> Handle(GetManifest request, CancellationToken cancellationToken) - => new(manifestRead.GetManifest(request.CustomerId, request.Id, request.IfNoneMatch, request.PathOnly, - cancellationToken)); + => manifestRead.GetManifest(request.CustomerId, request.Id, request.IfNoneMatch, request.PathOnly, + cancellationToken); } diff --git a/src/IIIFPresentation/API/Features/Manifest/Requests/GetManifestHierarchical.cs b/src/IIIFPresentation/API/Features/Manifest/Requests/GetManifestHierarchical.cs index ecf19bc7e..a0810f1b1 100644 --- a/src/IIIFPresentation/API/Features/Manifest/Requests/GetManifestHierarchical.cs +++ b/src/IIIFPresentation/API/Features/Manifest/Requests/GetManifestHierarchical.cs @@ -3,7 +3,7 @@ using AWS.S3; using AWS.Settings; using Core.Streams; -using Mediator; +using MediatR; using Microsoft.Extensions.Options; using Models.Database.General; using Repository.Paths; @@ -24,7 +24,7 @@ public class GetManifestHierarchicalHandler( { private readonly AWSSettings settings = options.Value; - public async ValueTask Handle(GetManifestHierarchical request, + public async Task Handle(GetManifestHierarchical request, CancellationToken cancellationToken) { var manifest = request.Hierarchy.Manifest ?? diff --git a/src/IIIFPresentation/API/Features/Manifest/Requests/UpsertHierarchicalManifest.cs b/src/IIIFPresentation/API/Features/Manifest/Requests/UpsertHierarchicalManifest.cs index ebce1a270..1f80b9a78 100644 --- a/src/IIIFPresentation/API/Features/Manifest/Requests/UpsertHierarchicalManifest.cs +++ b/src/IIIFPresentation/API/Features/Manifest/Requests/UpsertHierarchicalManifest.cs @@ -3,7 +3,7 @@ using API.Helpers; using API.Infrastructure.Requests; using API.Settings; -using Mediator; +using MediatR; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Models.API.Manifest; @@ -43,7 +43,7 @@ public class UpsertHierarchicalManifestHandler( IOptions apiOptions) : IRequestHandler { - public async ValueTask Handle(UpsertHierarchicalManifest request, + public async Task Handle(UpsertHierarchicalManifest request, CancellationToken cancellationToken) { var (error, context) = await hierarchicalRequestHelper.PrepareForUpsert( diff --git a/src/IIIFPresentation/API/Features/Manifest/Requests/UpsertManifest.cs b/src/IIIFPresentation/API/Features/Manifest/Requests/UpsertManifest.cs index 29dbe2a31..9911850f6 100644 --- a/src/IIIFPresentation/API/Features/Manifest/Requests/UpsertManifest.cs +++ b/src/IIIFPresentation/API/Features/Manifest/Requests/UpsertManifest.cs @@ -1,6 +1,6 @@ using API.Helpers; using API.Infrastructure.Requests; -using Mediator; +using MediatR; using Microsoft.Extensions.Primitives; using Models.API.Manifest; @@ -28,7 +28,7 @@ public class UpsertManifest( public class UpsertManifestHandler(IManifestWrite manifestService, IRequestIdResolver requestIdResolver) : IRequestHandler { - public async ValueTask Handle(UpsertManifest request, CancellationToken cancellationToken) + public async Task Handle(UpsertManifest request, CancellationToken cancellationToken) { var (error, resolvedId) = requestIdResolver.ResolveAndValidate(request.CustomerId, request.PresentationManifest.Id, request.ManifestId); diff --git a/src/IIIFPresentation/API/Features/Storage/CollectionController.cs b/src/IIIFPresentation/API/Features/Storage/CollectionController.cs index 2a7e9b77b..4ac1df83d 100644 --- a/src/IIIFPresentation/API/Features/Storage/CollectionController.cs +++ b/src/IIIFPresentation/API/Features/Storage/CollectionController.cs @@ -11,7 +11,7 @@ using API.Infrastructure.Requests; using API.Settings; using Core.Helpers; -using Mediator; +using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs index d265c4e04..1ffe480ba 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/CreateCollection.cs @@ -1,6 +1,6 @@ using API.Helpers; using API.Infrastructure.Requests; -using Mediator; +using MediatR; using Models.API.Collection; namespace API.Features.Storage.Requests; @@ -21,7 +21,7 @@ public class CreateCollection(int customerId, PresentationCollection collection, public class CreateCollectionHandler(ICollectionWrite collectionService, IRequestIdResolver requestIdResolver) : IRequestHandler { - public async ValueTask Handle(CreateCollection request, CancellationToken cancellationToken) + public async Task Handle(CreateCollection request, CancellationToken cancellationToken) { var (error, resolvedId) = requestIdResolver.ResolveAndValidate(request.CustomerId, request.Collection.Id); if (error != null) return error; diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/CreateHierarchicalCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/CreateHierarchicalCollection.cs index bd570c3e6..edce151a4 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/CreateHierarchicalCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/CreateHierarchicalCollection.cs @@ -3,7 +3,7 @@ using API.Helpers; using API.Infrastructure.Requests; using API.Settings; -using Mediator; +using MediatR; using Microsoft.Extensions.Options; using Models.API.Collection; @@ -35,7 +35,7 @@ public class CreateHierarchicalCollectionHandler( IOptions apiOptions) : IRequestHandler { - public async ValueTask Handle(CreateHierarchicalCollection request, + public async Task Handle(CreateHierarchicalCollection request, CancellationToken cancellationToken) { var (error, context) = await hierarchicalRequestHelper.PrepareForCreate( diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/DeleteCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/DeleteCollection.cs index 755215221..4d8673ddb 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/DeleteCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/DeleteCollection.cs @@ -1,6 +1,6 @@ using API.Features.Common.Helpers; using Core; -using Mediator; +using MediatR; using Models; using Models.API.General; using Models.Database.Collections; @@ -21,7 +21,7 @@ public class DeleteCollectionHandler( ILogger logger) : IRequestHandler> { - public async ValueTask> Handle(DeleteCollection request, CancellationToken cancellationToken) + public async Task> Handle(DeleteCollection request, CancellationToken cancellationToken) { logger.LogDebug("Deleting collection {CollectionId} for customer {CustomerId}", request.CollectionId, request.CustomerId); diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs index 5f20728ca..eb2105a4f 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/GetCollection.cs @@ -5,7 +5,7 @@ using API.Infrastructure.Requests; using API.Settings; using AWS.Helpers; -using Mediator; +using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Models.API.Collection; @@ -39,7 +39,7 @@ public class GetCollectionHandler(PresentationContext dbContext, IIIIFS3Service SettingsBasedPathGenerator settingsBasedPathGenerator, IOptions options) : IRequestHandler> { - public async ValueTask> Handle(GetCollection request, + public async Task> Handle(GetCollection request, CancellationToken cancellationToken) { var collection = await dbContext.RetrieveCollectionWithParentAsync(request.Id, diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs index 68074ea5a..8762dffb5 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/GetHierarchicalCollection.cs @@ -7,7 +7,7 @@ using AWS.Settings; using Core.Streams; using IIIF.Presentation.V3; -using Mediator; +using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Models.Database.General; @@ -32,7 +32,7 @@ public class GetHierarchicalCollectionHandler( { private readonly AWSSettings settings = options.Value; - public async ValueTask Handle(GetHierarchicalCollection request, + public async Task Handle(GetHierarchicalCollection request, CancellationToken cancellationToken) { if (request.Hierarchy.CollectionId == null || request.Hierarchy.Collection == null) diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/SearchCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/SearchCollection.cs index 144167f52..72d61f52b 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/SearchCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/SearchCollection.cs @@ -4,7 +4,7 @@ using API.Features.Storage.Models; using API.Infrastructure.Requests; using API.Settings; -using Mediator; +using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Models.API.Collection; @@ -51,7 +51,7 @@ public class SearchCollectionHandler( { private readonly ApiSettings settings = options.Value; - public async ValueTask> Handle(SearchCollection request, + public async Task> Handle(SearchCollection request, CancellationToken cancellationToken) { // Only the collection itself is required - the search result is synthetic, so it carries none of the diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs index 0ed20e00d..8e06a7bb0 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/UpsertCollection.cs @@ -1,6 +1,6 @@ using API.Helpers; using API.Infrastructure.Requests; -using Mediator; +using MediatR; using Models.API.Collection; namespace API.Features.Storage.Requests; @@ -25,7 +25,7 @@ public class UpsertCollection(int customerId, string collectionId, PresentationC public class UpsertCollectionHandler(ICollectionWrite collectionService, IRequestIdResolver requestIdResolver) : IRequestHandler { - public async ValueTask Handle(UpsertCollection request, CancellationToken cancellationToken) + public async Task Handle(UpsertCollection request, CancellationToken cancellationToken) { var (error, resolvedId) = requestIdResolver.ResolveAndValidate(request.CustomerId, request.Collection.Id, request.CollectionId); diff --git a/src/IIIFPresentation/API/Features/Storage/Requests/UpsertHierarchicalCollection.cs b/src/IIIFPresentation/API/Features/Storage/Requests/UpsertHierarchicalCollection.cs index 9d18da9e7..cdebb58e8 100644 --- a/src/IIIFPresentation/API/Features/Storage/Requests/UpsertHierarchicalCollection.cs +++ b/src/IIIFPresentation/API/Features/Storage/Requests/UpsertHierarchicalCollection.cs @@ -3,7 +3,7 @@ using API.Features.Storage.Validators; using API.Infrastructure.Requests; using API.Settings; -using Mediator; +using MediatR; using Microsoft.Extensions.Options; using Models.API.Collection; using DbCollection = Models.Database.Collections.Collection; @@ -38,7 +38,7 @@ public class UpsertHierarchicalCollectionHandler( IOptions apiOptions) : IRequestHandler { - public async ValueTask Handle(UpsertHierarchicalCollection request, + public async Task Handle(UpsertHierarchicalCollection request, CancellationToken cancellationToken) { var (error, context) = await hierarchicalRequestHelper.PrepareForUpsert( diff --git a/src/IIIFPresentation/API/Features/Storage/StorageController.cs b/src/IIIFPresentation/API/Features/Storage/StorageController.cs index 3b3cc2367..252d2a121 100644 --- a/src/IIIFPresentation/API/Features/Storage/StorageController.cs +++ b/src/IIIFPresentation/API/Features/Storage/StorageController.cs @@ -9,7 +9,7 @@ using API.Infrastructure.Http; using API.Infrastructure.Requests; using API.Settings; -using Mediator; +using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.WebUtilities; diff --git a/src/IIIFPresentation/API/Infrastructure/PresentationController.cs b/src/IIIFPresentation/API/Infrastructure/PresentationController.cs index 2f23269ca..9365238e3 100644 --- a/src/IIIFPresentation/API/Infrastructure/PresentationController.cs +++ b/src/IIIFPresentation/API/Infrastructure/PresentationController.cs @@ -4,7 +4,7 @@ using API.Settings; using Core; using IIIF; -using Mediator; +using MediatR; using Microsoft.AspNetCore.Mvc; using Models.API.General; @@ -73,7 +73,7 @@ protected async Task HandleUpsert( /// /// Handles a deletion /// - /// The request/response to be sent through Mediator + /// The request/response to be sent through Mediatr /// The title of the error /// Current cancellation token /// Thrown when the is not understood diff --git a/src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/CacheInvalidationBehaviour.cs b/src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/CacheInvalidationBehaviour.cs index 4775b816c..7f73758ef 100644 --- a/src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/CacheInvalidationBehaviour.cs +++ b/src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/CacheInvalidationBehaviour.cs @@ -1,5 +1,5 @@ -using LazyCache; -using Mediator; +using LazyCache; +using MediatR; namespace API.Infrastructure.Requests.Pipelines; @@ -15,19 +15,19 @@ public interface IInvalidateCaches } /// -/// Mediator behaviour that will clear cacheKeys specified in request if request was successful +/// MediatR behaviour that will clear cacheKeys specified in request if request was successful /// public class CacheInvalidationBehaviour( IAppCache appCache, ILogger> logger) : IPipelineBehavior - where TRequest : IInvalidateCaches, IRequest + where TRequest : notnull, IInvalidateCaches, IRequest where TResponse : IModifyRequest { - public async ValueTask Handle(TRequest request, MessageHandlerDelegate next, + public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { - var nextResponse = await next(request, cancellationToken); + var nextResponse = await next(); if (nextResponse.IsSuccess) InvalidateCacheKeys(request); diff --git a/src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/LoggingBehaviour.cs b/src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/LoggingBehaviour.cs index 8fcd57eaa..548d0938b 100644 --- a/src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/LoggingBehaviour.cs +++ b/src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/LoggingBehaviour.cs @@ -1,24 +1,24 @@ -using System.Diagnostics; -using Mediator; +using System.Diagnostics; +using MediatR; namespace API.Infrastructure.Requests.Pipelines; /// -/// Mediator pipeline behaviour that logs requests with timings. +/// MediatR pipeline behaviour that logs requests with timings. /// Will use ToString() property to log details /// public class LoggingBehavior(ILogger> logger) : IPipelineBehavior - where TRequest : IRequest, IBaseRequest + where TRequest : notnull, IRequest { - public async ValueTask Handle(TRequest request, MessageHandlerDelegate next, + public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { // This could be cleverer, currently will just log ToString() logger.LogTrace("Handling '{RequestType}' request. {Request}", typeof(TRequest).Name, request); var sw = Stopwatch.StartNew(); - var response = await next(request, cancellationToken); + var response = await next(); sw.Stop(); logger.LogTrace("Handled '{RequestType}' in {Elapsed}ms. {Request}", typeof(TRequest).Name, diff --git a/src/IIIFPresentation/API/Infrastructure/ServiceCollectionX.cs b/src/IIIFPresentation/API/Infrastructure/ServiceCollectionX.cs index 44e5507a8..03669f47a 100644 --- a/src/IIIFPresentation/API/Infrastructure/ServiceCollectionX.cs +++ b/src/IIIFPresentation/API/Infrastructure/ServiceCollectionX.cs @@ -1,10 +1,11 @@ -using API.Infrastructure.IdGenerator; +using System.Reflection; +using API.Infrastructure.IdGenerator; using API.Infrastructure.Requests.Pipelines; using API.Settings; using AWS.Configuration; using AWS.Helpers; using AWS.S3; -using Mediator; +using MediatR; using Microsoft.OpenApi; using Repository; using Sqids; @@ -37,18 +38,14 @@ public IServiceCollection AddCaching(CacheSettings cacheSettings) .AddLazyCache(); /// - /// Add Mediator services and pipeline behaviours to service collection. + /// Add MediatR services and pipeline behaviours to service collection. /// - public IServiceCollection ConfigureMediator() + public IServiceCollection ConfigureMediatR() { return services - .AddMediator(options => - { - // Handlers depend on scoped services (PresentationContext etc); Mediator defaults to - // Singleton, which would capture those as long-lived dependencies. - options.ServiceLifetime = ServiceLifetime.Scoped; - options.PipelineBehaviors = [typeof(LoggingBehavior<,>), typeof(CacheInvalidationBehaviour<,>)]; - }); + .AddMediatR(config => config.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())) + .AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)) + .AddScoped(typeof(IPipelineBehavior<,>), typeof(CacheInvalidationBehaviour<,>)); } /// diff --git a/src/IIIFPresentation/API/Program.cs b/src/IIIFPresentation/API/Program.cs index 15e1fa28b..2d4c5a737 100644 --- a/src/IIIFPresentation/API/Program.cs +++ b/src/IIIFPresentation/API/Program.cs @@ -101,7 +101,7 @@ .AddHttpContextAccessor() .AddScoped() .AddOutgoingHeaders(); -builder.Services.ConfigureMediator(); +builder.Services.ConfigureMediatR(); builder.Services.ConfigureIdGenerator(); builder.Services .AddHealthChecks() diff --git a/src/IIIFPresentation/Repository/Repository.csproj b/src/IIIFPresentation/Repository/Repository.csproj index ef25b767b..5872892b6 100644 --- a/src/IIIFPresentation/Repository/Repository.csproj +++ b/src/IIIFPresentation/Repository/Repository.csproj @@ -11,6 +11,7 @@ + From 1e356d3d8522c7d30315792b8532cfe585c876a3 Mon Sep 17 00:00:00 2001 From: Donald Gray Date: Fri, 21 Aug 2026 08:53:40 +0100 Subject: [PATCH 5/7] Use aspnet:10.0-noble rather than aspnet:10.0 These are both identical images but the former is self documenting and consistent with other projects in org --- Dockerfile.API | 2 +- Dockerfile.BackgroundHandler | 2 +- Dockerfile.Migrator | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile.API b/Dockerfile.API index 1336b50da..0b38654e6 100644 --- a/Dockerfile.API +++ b/Dockerfile.API @@ -15,7 +15,7 @@ RUN dotnet build "API.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "API.csproj" -c Release -r linux-x64 --self-contained false -o /app/publish -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble AS base ARG APP_VERSION=dev ENV APP_VERSION=${APP_VERSION} diff --git a/Dockerfile.BackgroundHandler b/Dockerfile.BackgroundHandler index 8e5f7caae..618e147e5 100644 --- a/Dockerfile.BackgroundHandler +++ b/Dockerfile.BackgroundHandler @@ -15,7 +15,7 @@ RUN dotnet build "BackgroundHandler.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "BackgroundHandler.csproj" -c Release -o /app/publish -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble AS base LABEL maintainer="Donald Gray , Jack Lewis " LABEL org.opencontainers.image.source=https://github.com/dlcs/iiif-presentation diff --git a/Dockerfile.Migrator b/Dockerfile.Migrator index 8b0a05ab0..a2f277e6d 100644 --- a/Dockerfile.Migrator +++ b/Dockerfile.Migrator @@ -15,7 +15,7 @@ RUN dotnet build "Migrator.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "Migrator.csproj" -c Release -o /app/publish -FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble AS base LABEL maintainer="Donald Gray , Jack Lewis " LABEL org.opencontainers.image.source=https://github.com/dlcs/iiif-presentation From 313867f61e8c22488f33de00f71d4cb82194412c Mon Sep 17 00:00:00 2001 From: Donald Gray Date: Fri, 21 Aug 2026 09:20:58 +0100 Subject: [PATCH 6/7] Couple of tweaks following dotnet 10 upgrade * Set checksum override for SQS clients using localstack. I don't think this is ever actually exercised but would cause issues if project was run against localstack locally. * Change nullability of s3 headers in our mapped object to map downstream * Remove now defunct xml header --- .../API/Infrastructure/PresentationController.cs | 1 - src/IIIFPresentation/AWS/Configuration/AWSConfiguration.cs | 4 +++- src/IIIFPresentation/AWS/S3/Models/ObjectFromBucket.cs | 4 ++-- src/IIIFPresentation/AWS/S3/S3Extensions.cs | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/IIIFPresentation/API/Infrastructure/PresentationController.cs b/src/IIIFPresentation/API/Infrastructure/PresentationController.cs index 9365238e3..debc8bea3 100644 --- a/src/IIIFPresentation/API/Infrastructure/PresentationController.cs +++ b/src/IIIFPresentation/API/Infrastructure/PresentationController.cs @@ -81,7 +81,6 @@ protected async Task HandleUpsert( /// ActionResult generated from DeleteResult. This will be 204 on success. Or an /// error and appropriate status code if failed. /// - /// This will be replaced with overload that takes DeleteEntityResult in future protected async Task HandleDelete( IRequest> request, string? errorTitle = "Delete failed", diff --git a/src/IIIFPresentation/AWS/Configuration/AWSConfiguration.cs b/src/IIIFPresentation/AWS/Configuration/AWSConfiguration.cs index 56e1885df..3bdbd7648 100644 --- a/src/IIIFPresentation/AWS/Configuration/AWSConfiguration.cs +++ b/src/IIIFPresentation/AWS/Configuration/AWSConfiguration.cs @@ -107,6 +107,8 @@ public AwsBuilder WithAmazonSQS(ServiceLifetime lifetime = ServiceLifetime.Singl RegionEndpoint = RegionEndpoint.USEast1, ServiceURL = awsSettings.SQS?.ServiceUrl.ThrowIfNullOrWhiteSpace(nameof(awsSettings.SQS.ServiceUrl)), + RequestChecksumCalculation = RequestChecksumCalculation.WHEN_REQUIRED, + ResponseChecksumValidation = ResponseChecksumValidation.WHEN_REQUIRED }; return new AmazonSQSClient(new BasicAWSCredentials("foo", "bar"), amazonS3Config); }, lifetime); @@ -119,4 +121,4 @@ public AwsBuilder WithAmazonSQS(ServiceLifetime lifetime = ServiceLifetime.Singl return this; } -} \ No newline at end of file +} diff --git a/src/IIIFPresentation/AWS/S3/Models/ObjectFromBucket.cs b/src/IIIFPresentation/AWS/S3/Models/ObjectFromBucket.cs index 7f4565277..2bed870c3 100644 --- a/src/IIIFPresentation/AWS/S3/Models/ObjectFromBucket.cs +++ b/src/IIIFPresentation/AWS/S3/Models/ObjectFromBucket.cs @@ -42,6 +42,6 @@ public class ObjectInBucketHeaders public string? ContentMD5 { get; set; } public string? ContentType { get; set; } public DateTime? ExpiresUtc { get; set; } - public DateTime LastModified { get; set; } + public DateTime? LastModified { get; set; } public string ETag { get; set; } -} \ No newline at end of file +} diff --git a/src/IIIFPresentation/AWS/S3/S3Extensions.cs b/src/IIIFPresentation/AWS/S3/S3Extensions.cs index 224f8e315..06e4541ed 100644 --- a/src/IIIFPresentation/AWS/S3/S3Extensions.cs +++ b/src/IIIFPresentation/AWS/S3/S3Extensions.cs @@ -65,7 +65,7 @@ private static ObjectInBucketHeaders AsObjectInBucketHeaders(this GetObjectRespo ContentType = headersCollection.ContentType, ExpiresUtc = headersCollection.Expires, ETag = getObjectResponse.ETag, - LastModified = getObjectResponse.LastModified.GetValueOrDefault(), + LastModified = getObjectResponse.LastModified, }; return fromHeaders; } From 2dbd39aaf4a0e94ff31fbc5465eaa4d8df02a21a Mon Sep 17 00:00:00 2001 From: Donald Gray Date: Fri, 21 Aug 2026 13:46:41 +0100 Subject: [PATCH 7/7] Handle AWS v3->v4 returning null collections Change in behaviour for 3->4, it will now return null, whereas previously it would return a empty --- src/IIIFPresentation/AWS/S3/S3BucketReader.cs | 2 +- src/IIIFPresentation/AWS/S3/S3BucketWriter.cs | 2 +- src/IIIFPresentation/AWS/SQS/SqsListener.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/IIIFPresentation/AWS/S3/S3BucketReader.cs b/src/IIIFPresentation/AWS/S3/S3BucketReader.cs index 5a7953fd3..2179f5dbf 100644 --- a/src/IIIFPresentation/AWS/S3/S3BucketReader.cs +++ b/src/IIIFPresentation/AWS/S3/S3BucketReader.cs @@ -68,7 +68,7 @@ public async Task GetMatchingKeys(ObjectInBucket rootKey) try { var response = await s3Client.ListObjectsAsync(listObjectsRequest, CancellationToken.None); - return response.S3Objects.Select(obj => obj.Key).OrderBy(s => s).ToArray(); + return response.S3Objects?.Select(obj => obj.Key).OrderBy(s => s).ToArray() ?? []; } catch (AmazonS3Exception e) when (e.StatusCode == HttpStatusCode.NotFound) { diff --git a/src/IIIFPresentation/AWS/S3/S3BucketWriter.cs b/src/IIIFPresentation/AWS/S3/S3BucketWriter.cs index 530725f21..18af0b799 100644 --- a/src/IIIFPresentation/AWS/S3/S3BucketWriter.cs +++ b/src/IIIFPresentation/AWS/S3/S3BucketWriter.cs @@ -76,7 +76,7 @@ public async Task DeleteFolder(ObjectInBucket root, bool deleteRoot) do { listObjectsResponse = await s3Client.ListObjectsAsync(listObjectsRequest); - foreach (var item in listObjectsResponse.S3Objects.OrderBy(x => x.Key)) + foreach (var item in (listObjectsResponse.S3Objects ?? []).OrderBy(x => x.Key)) { deleteObjectsRequest.AddKey(item.Key); if (deleteObjectsRequest.Objects.Count == 1000) diff --git a/src/IIIFPresentation/AWS/SQS/SqsListener.cs b/src/IIIFPresentation/AWS/SQS/SqsListener.cs index 9b87b8be1..f233c0fd5 100644 --- a/src/IIIFPresentation/AWS/SQS/SqsListener.cs +++ b/src/IIIFPresentation/AWS/SQS/SqsListener.cs @@ -99,8 +99,8 @@ private async Task HandleMessage(string queueUrl, Message message, Canc { try { - var queueMessage = new QueueMessage(GetJsonPayload(message), message.MessageAttributes, message.Attributes, - message.MessageId); + var queueMessage = new QueueMessage(GetJsonPayload(message), message.MessageAttributes ?? [], + message.Attributes ?? [], message.MessageId); // create a new scope to avoid issues with Scoped dependencies using var listenerScope = serviceScopeFactory.CreateScope();