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..0b38654e6 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-noble AS base ARG APP_VERSION=dev ENV APP_VERSION=${APP_VERSION} diff --git a/Dockerfile.BackgroundHandler b/Dockerfile.BackgroundHandler index 5763981ed..618e147e5 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-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 108de6bc3..a2f277e6d 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-noble 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/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 8bc0a57c6..4d8673ddb 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 MediatR; 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 c25bbf8ea..debc8bea3 100644 --- a/src/IIIFPresentation/API/Infrastructure/PresentationController.cs +++ b/src/IIIFPresentation/API/Infrastructure/PresentationController.cs @@ -6,6 +6,7 @@ using IIIF; using MediatR; 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 @@ -80,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", @@ -95,30 +95,6 @@ protected async Task HandleDelete( }, errorTitle); } - /// - /// Handles a deletion, turning DeleteResult to a http response - /// - /// The request/response to be sent through Mediatr - /// 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 +116,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 d7907211e..7f73758ef 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 LazyCache; using MediatR; 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 { @@ -17,20 +17,13 @@ public interface IInvalidateCaches /// /// MediatR behaviour that will clear cacheKeys specified in request if request was successful /// -public class CacheInvalidationBehaviour : IPipelineBehavior - where TRequest : IInvalidateCaches, IRequest +public class CacheInvalidationBehaviour( + IAppCache appCache, + ILogger> logger) + : IPipelineBehavior + where TRequest : notnull, 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 Task Handle(TRequest request, RequestHandlerDelegate 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/Mediatr/Behaviours/LoggingBehaviour.cs b/src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/LoggingBehaviour.cs similarity index 59% rename from src/IIIFPresentation/API/Infrastructure/Mediatr/Behaviours/LoggingBehaviour.cs rename to src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/LoggingBehaviour.cs index 456be17dd..548d0938b 100644 --- a/src/IIIFPresentation/API/Infrastructure/Mediatr/Behaviours/LoggingBehaviour.cs +++ b/src/IIIFPresentation/API/Infrastructure/Requests/Pipelines/LoggingBehaviour.cs @@ -1,22 +1,16 @@ -using System.Diagnostics; +using System.Diagnostics; using MediatR; -namespace API.Infrastructure.Mediatr.Behaviours; +namespace API.Infrastructure.Requests.Pipelines; /// -/// Mediatr 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 : IPipelineBehavior - where TRequest : IRequest, IBaseRequest +public class LoggingBehavior(ILogger> logger) + : IPipelineBehavior + where TRequest : notnull, IRequest { - private readonly ILogger> logger; - - public LoggingBehavior(ILogger> logger) - { - this.logger = logger; - } - public async Task Handle(TRequest request, RequestHandlerDelegate 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 262cdeb1b..03669f47a 100644 --- a/src/IIIFPresentation/API/Infrastructure/ServiceCollectionX.cs +++ b/src/IIIFPresentation/API/Infrastructure/ServiceCollectionX.cs @@ -1,13 +1,12 @@ using System.Reflection; using API.Infrastructure.IdGenerator; -using API.Infrastructure.Mediatr.Behaviours; using API.Infrastructure.Requests.Pipelines; using API.Settings; using AWS.Configuration; using AWS.Helpers; using AWS.S3; using MediatR; -using Microsoft.OpenApi.Models; +using Microsoft.OpenApi; using Repository; using Sqids; @@ -15,124 +14,113 @@ 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 MediatR services and pipeline behaviours to service collection. - /// - public static IServiceCollection ConfigureMediatR(this IServiceCollection services) + /// Current object + extension(IServiceCollection services) { - return services - .AddMediatR(config => config.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())) - .AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)) - .AddScoped(typeof(IPipelineBehavior<,>), 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 MediatR services and pipeline behaviours to service collection. + /// + public IServiceCollection ConfigureMediatR() + { + return services + .AddMediatR(config => config.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())) + .AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)) + .AddScoped(typeof(IPipelineBehavior<,>), 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 - { - Title = "IIIF Presentation API", - Version = "v1", - Description = "API for creation and management of IIIF Presentation API resources" - }); + services + .AddSingleton() + .AddSingleton() + .AddSingleton(); - c.AddSecurityDefinition( - "basic", new OpenApiSecurityScheme - { - Name = "Authorization", - Type = SecuritySchemeType.Http, - Scheme = "basic", - In = ParameterLocation.Header, - Description = "Basic Authorization header", - }); + services + .SetupAWS(configuration, webHostEnvironment) + .WithAmazonS3(); - c.AddSecurityRequirement(new OpenApiSecurityRequirement + return services; + } + + /// + /// Add Cors policy allowing any Origin, Method and Header + /// + /// Cors policy name + public IServiceCollection ConfigureDefaultCors(string policyName) + => services.AddCors(options => { + options.AddPolicy(policyName, builder => builder + .AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader()); + }); + + /// + /// Add SwaggerGen services to service collection. + /// + public IServiceCollection ConfigureSwagger() + => services + .AddEndpointsApiExplorer() + .AddSwaggerGen(c => { - new OpenApiSecurityScheme + c.SwaggerDoc("v1", new OpenApiInfo { - Reference = new OpenApiReference + Title = "IIIF Presentation API", + Version = "v1", + Description = "API for creation and management of IIIF Presentation API resources" + }); + + c.AddSecurityDefinition( + "basic", new OpenApiSecurityScheme { - Type = ReferenceType.SecurityScheme, - Id = "basic", - }, - Scheme = "basic", - Name = "Authorization", - In = ParameterLocation.Header - }, - [] - }, - }); - }); + 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 0d1974778..2d4c5a737 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/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..3bdbd7648 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); @@ -105,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); @@ -117,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/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 3546c2a64..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) @@ -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..06e4541ed 100644 --- a/src/IIIFPresentation/AWS/S3/S3Extensions.cs +++ b/src/IIIFPresentation/AWS/S3/S3Extensions.cs @@ -63,7 +63,7 @@ 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, }; 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(); 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/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); + } } } 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..5872892b6 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 @@ - - - - - - - - - + + + + + + + + +