diff --git a/Imageflow.Server.sln b/Imageflow.Server.sln index 5f95a95..3a023fb 100644 --- a/Imageflow.Server.sln +++ b/Imageflow.Server.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29503.13 @@ -64,6 +64,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Imazen.Routing", "src\Imaze EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Imazen.Abstractions", "src\Imazen.Abstractions\Imazen.Abstractions.csproj", "{A04B9BE0-4931-4305-B9AB-B79737130F20}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Imageflow.Server.ExampleModernAPI", "examples\Imageflow.Server.ExampleModernAPI\Imageflow.Server.ExampleModernAPI.csproj", "{4AF9EFF8-5456-4711-B847-6DD31F949B02}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -158,6 +160,10 @@ Global {A04B9BE0-4931-4305-B9AB-B79737130F20}.Debug|Any CPU.Build.0 = Debug|Any CPU {A04B9BE0-4931-4305-B9AB-B79737130F20}.Release|Any CPU.ActiveCfg = Release|Any CPU {A04B9BE0-4931-4305-B9AB-B79737130F20}.Release|Any CPU.Build.0 = Release|Any CPU + {4AF9EFF8-5456-4711-B847-6DD31F949B02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4AF9EFF8-5456-4711-B847-6DD31F949B02}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4AF9EFF8-5456-4711-B847-6DD31F949B02}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4AF9EFF8-5456-4711-B847-6DD31F949B02}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/examples/Imageflow.Server.ExampleModernAPI/CustomMediaEndpoint.cs b/examples/Imageflow.Server.ExampleModernAPI/CustomMediaEndpoint.cs new file mode 100644 index 0000000..9d9649e --- /dev/null +++ b/examples/Imageflow.Server.ExampleModernAPI/CustomMediaEndpoint.cs @@ -0,0 +1,204 @@ +using System.Buffers; +using System.Text; +using System.Text.Json; +using Imazen.Abstractions.Blobs; +using Imazen.Abstractions.Resulting; +using Imazen.Routing.HttpAbstractions; +using Imazen.Routing.Layers; +using Imazen.Routing.Promises; +using Imazen.Routing.Requests; + +namespace Imageflow.Server.ExampleModernAPI; + + +internal record CustomFileData(string Path1, string QueryString1, string Path2, string QueryString2); + +/// +/// This layer will capture requests for .json.custom paths. No .custom file actually exists, but the .json does, and we'll use that to determine the dependencies. +/// +public class CustomMediaLayer(PathMapper jsonFileMapper) : Imazen.Routing.Layers.IRoutingLayer +{ + public string Name => ".json.custom file handler"; + + public IFastCond? FastPreconditions => Conditions.HasPathSuffixOrdinalIgnoreCase(".json.custom"); + public ValueTask?> ApplyRouting(MutableRequest request, CancellationToken cancellationToken = default) + { + // FastPreconditions should have already been checked + var result = jsonFileMapper.TryMapVirtualPath(request.Path.Replace(".json.custom", ".json")); + if (result == null) + { + // no mapping found + return new ValueTask?>((CodeResult?)null); + } + var physicalPath = result.Value.MappedPhysicalPath; + var lastWriteTimeUtc = File.GetLastWriteTimeUtc(physicalPath); + if (lastWriteTimeUtc.Year == 1601) // file doesn't exist, pass to next middleware + { + return new ValueTask?>((CodeResult?)null); + } + // Ok, the file exists. We can load and parse it using System.Text.Json to determine the dependencies.\ + return RouteFromJsonFile(physicalPath, lastWriteTimeUtc, result.Value.MappingUsed, request, cancellationToken); + } + + private async ValueTask?> RouteFromJsonFile(string jsonFilePath, DateTime lastWriteTimeUtc, IPathMapping mappingUsed, MutableRequest request, CancellationToken cancellationToken) + { + // TODO: here, we could cache the json files in memory using a key based on jsonFilePath and lastWriteTimeUtc. + + var jsonText = await File.ReadAllTextAsync(jsonFilePath, cancellationToken); + var data = JsonSerializer.Deserialize(jsonText); + if (data == null) + { + return CodeResult.Err((HttpStatus.ServerError, "Failed to parse .json custom data file")); + } + + return new PromiseWrappingEndpoint(new CustomMediaPromise(request.ToSnapshot(true),data)); + } +} + +internal class CustomMediaPromise(IRequestSnapshot r, CustomFileData data) : ICacheableBlobPromise +{ + public bool IsCacheSupporting => true; + public IRequestSnapshot FinalRequest { get; } = r; + + public async ValueTask CreateResponseAsync(IRequestSnapshot request, IBlobRequestRouter router, IBlobPromisePipeline pipeline, + CancellationToken cancellationToken = default) + { + // This code path isn't called, it's just to satisfy the primitive IInstantPromise interface. + return new BlobResponse(await TryGetBlobAsync(request, router, pipeline, cancellationToken)); + } + + public bool HasDependencies => true; + public bool ReadyToWriteCacheKeyBasisData { get; private set; } + + /// + /// Gets a promise for the given path that includes caching logic if indicated by the caching configuration and the latency by default. + /// + /// + /// + /// + /// + private async ValueTask> RouteDependencyAsync(IBlobRequestRouter router, string childRequestUri, + CancellationToken cancellationToken = default) + { + if (FinalRequest.OriginatingRequest == null) + { + return CodeResult.ErrFrom(HttpStatus.BadRequest, "OriginatingRequest is required, but was null"); + } + var dependencyRequest = MutableRequest.ChildRequest(FinalRequest.OriginatingRequest, FinalRequest, childRequestUri, HttpMethods.Get); + var routingResult = await router.RouteToPromiseAsync(dependencyRequest, cancellationToken); + if (routingResult == null) + { + return CodeResult.ErrFrom(HttpStatus.NotFound, "Dependency not found: " + childRequestUri); + } + if (routingResult.TryUnwrapError(out var error)) + { + return CodeResult.Err(error.WithAppend("Error routing to dependency: " + childRequestUri)); + } + return CodeResult.Ok(routingResult.Unwrap()); + } + public async ValueTask RouteDependenciesAsync(IBlobRequestRouter router, CancellationToken cancellationToken = default) + { + var uri1 = data.Path1 + data.QueryString1; + var uri2 = data.Path2 + data.QueryString2; + + foreach (var uri in new[]{uri1, uri2}) + { + var routingResult = await RouteDependencyAsync(router, uri, cancellationToken); + if (routingResult.TryUnwrapError(out var error)) + { + return CodeResult.Err(error); + } + Dependencies ??= new List(); + Dependencies.Add(routingResult.Unwrap()); + } + ReadyToWriteCacheKeyBasisData = true; + return CodeResult.Ok(); + } + + internal List? Dependencies { get; private set; } + + private LatencyTrackingZone? latencyZone = null; + /// + /// Must route dependencies first! + /// + public LatencyTrackingZone? LatencyZone { + get + { + if (!ReadyToWriteCacheKeyBasisData) throw new InvalidOperationException("Dependencies must be routed first"); + // produce a latency zone based on all dependency strings, joined, plus the sum of their latency defaults + if (latencyZone != null) return latencyZone; + var latency = 0; + var sb = new StringBuilder(); + sb.Append("customMediaSwitcher("); + foreach (var dependency in Dependencies!) + { + latency += dependency.LatencyZone?.DefaultMs ?? 0; + sb.Append(dependency.LatencyZone?.TrackingZone ?? "(unknown)"); + } + sb.Append(")"); + latencyZone = new LatencyTrackingZone(sb.ToString(), latency, true); //AlwaysShield is true (never skip caching) + return latencyZone; + } + } + + public void WriteCacheKeyBasisPairsToRecursive(IBufferWriter writer) + { + FinalRequest.WriteCacheKeyBasisPairsTo(writer); + if (Dependencies == null) throw new InvalidOperationException("Dependencies must be routed first"); + foreach (var dependency in Dependencies) + { + dependency.WriteCacheKeyBasisPairsToRecursive(writer); + } + + var otherCacheKeyData = 1; + writer.WriteInt(otherCacheKeyData); + } + + private byte[]? cacheKey32Bytes = null; + public byte[] GetCacheKey32Bytes() + { + return cacheKey32Bytes ??= this.GetCacheKey32BytesUncached(); + } + + public bool SupportsPreSignedUrls => false; + + public async ValueTask> TryGetBlobAsync(IRequestSnapshot request, IBlobRequestRouter router, IBlobPromisePipeline pipeline, + CancellationToken cancellationToken = default) + { + // Our logic is to return whichever dependency is smaller. + // This is a contrived example, but it's a good example of how to use dependencies. + var blobWrappers = new List(); + var smallestBlob = default(IBlobWrapper); + try + { + foreach (var dependency in Dependencies!) + { + var result = await dependency.TryGetBlobAsync(request, router, pipeline, cancellationToken); + if (result.TryUnwrapError(out var error)) + { + return CodeResult.Err(error); + } + var blob = result.Unwrap(); + blobWrappers.Add(blob); + + if (smallestBlob == null || blob.Attributes.EstimatedBlobByteCount < smallestBlob.Attributes.EstimatedBlobByteCount) + { + smallestBlob = blob; + } + } + if (smallestBlob == null) + { + return CodeResult.ErrFrom(HttpStatus.NotFound, "No dependencies found"); + } + return CodeResult.Ok(smallestBlob.ForkReference()); + } + finally + { + foreach (var blobWrapper in blobWrappers) + { + blobWrapper.Dispose(); + } + } + } +} + diff --git a/examples/Imageflow.Server.ExampleModernAPI/Dockerfile b/examples/Imageflow.Server.ExampleModernAPI/Dockerfile new file mode 100644 index 0000000..9fad555 --- /dev/null +++ b/examples/Imageflow.Server.ExampleModernAPI/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["examples/Imageflow.Server.ExampleModernAPI/Imageflow.Server.ExampleModernAPI.csproj", "examples/Imageflow.Server.ExampleModernAPI/"] +RUN dotnet restore "examples/Imageflow.Server.ExampleModernAPI/Imageflow.Server.ExampleModernAPI.csproj" +COPY . . +WORKDIR "/src/examples/Imageflow.Server.ExampleModernAPI" +RUN dotnet build "Imageflow.Server.ExampleModernAPI.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "Imageflow.Server.ExampleModernAPI.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Imageflow.Server.ExampleModernAPI.dll"] diff --git a/examples/Imageflow.Server.ExampleModernAPI/Imageflow.Server.ExampleModernAPI.csproj b/examples/Imageflow.Server.ExampleModernAPI/Imageflow.Server.ExampleModernAPI.csproj new file mode 100644 index 0000000..f8e824a --- /dev/null +++ b/examples/Imageflow.Server.ExampleModernAPI/Imageflow.Server.ExampleModernAPI.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + enable + enable + Linux + + + + + + + + + + .dockerignore + + + + + + + + + + + + + + + diff --git a/examples/Imageflow.Server.ExampleModernAPI/Imageflow.Server.ExampleModernAPI.http b/examples/Imageflow.Server.ExampleModernAPI/Imageflow.Server.ExampleModernAPI.http new file mode 100644 index 0000000..15c38d1 --- /dev/null +++ b/examples/Imageflow.Server.ExampleModernAPI/Imageflow.Server.ExampleModernAPI.http @@ -0,0 +1,6 @@ +@Imageflow.Server.ExampleModernAPI_HostAddress = http://localhost:5025 + +GET {{Imageflow.Server.ExampleModernAPI_HostAddress}}/img/test.json.custom + + +### diff --git a/examples/Imageflow.Server.ExampleModernAPI/Program.cs b/examples/Imageflow.Server.ExampleModernAPI/Program.cs new file mode 100644 index 0000000..50ac0c0 --- /dev/null +++ b/examples/Imageflow.Server.ExampleModernAPI/Program.cs @@ -0,0 +1,70 @@ +using Imageflow.Fluent; +using Imageflow.Server; +using Imageflow.Server.ExampleModernAPI; +using Imazen.Abstractions.Logging; +using Imazen.Routing.Layers; +using PathMapping = Imazen.Routing.Layers.PathMapping; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +builder.Services.AddImageflowLoggingSupport(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + + +app.UseImageflow(new ImageflowMiddlewareOptions() + .MapPath("/images", Path.Join(builder.Environment.WebRootPath, "images")) + .SetMyOpenSourceProjectUrl("https://github.com/imazen/imageflow-dotnet-server") + .AddRoutingConfiguration((routing) => + { + routing.ConfigureEndpoints((endpoints) => + { + endpoints.AddLayer(new CustomMediaLayer(new PathMapper(new[] + { + new PathMapping("/img/", Path.Join(builder.Environment.ContentRootPath, "json"), true) + }))); + }); + })); + + +var summaries = new[] +{ + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" +}; + +app.MapGet("/weatherforecast", () => + { + var forecast = Enumerable.Range(1, 5).Select(index => + new WeatherForecast + ( + DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + Random.Shared.Next(-20, 55), + summaries[Random.Shared.Next(summaries.Length)] + )) + .ToArray(); + return forecast; + }) + .WithName("GetWeatherForecast") + .WithOpenApi(); + +app.Run(); + +record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) +{ + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); +} + diff --git a/examples/Imageflow.Server.ExampleModernAPI/appsettings.Development.json b/examples/Imageflow.Server.ExampleModernAPI/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/examples/Imageflow.Server.ExampleModernAPI/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/examples/Imageflow.Server.ExampleModernAPI/appsettings.json b/examples/Imageflow.Server.ExampleModernAPI/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/examples/Imageflow.Server.ExampleModernAPI/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/examples/Imageflow.Server.ExampleModernAPI/json/test.json b/examples/Imageflow.Server.ExampleModernAPI/json/test.json new file mode 100644 index 0000000..d5ac848 --- /dev/null +++ b/examples/Imageflow.Server.ExampleModernAPI/json/test.json @@ -0,0 +1,6 @@ +{ + "Path1": "/images/fire.jpg", + "Path2": "/images/fire.jpg", + "Querystring1": "format=webp&quality=80", + "Querystring2": "format=jpeg&quality=76" +} \ No newline at end of file diff --git a/examples/Imageflow.Server.ExampleModernAPI/packages.lock.json b/examples/Imageflow.Server.ExampleModernAPI/packages.lock.json new file mode 100644 index 0000000..72d37f5 --- /dev/null +++ b/examples/Imageflow.Server.ExampleModernAPI/packages.lock.json @@ -0,0 +1,232 @@ +{ + "version": 1, + "dependencies": { + "net8.0": { + "Microsoft.AspNetCore.OpenApi": { + "type": "Direct", + "requested": "[8.0.3, )", + "resolved": "8.0.3", + "contentHash": "mUq0UL+H7UtA3Jud/7/BC7n5W2c4zCvTFUa2hE2+/oQqATa4oGHb87ETON09SEWkcbBRTz3WM16kTE+zuoXq2A==", + "dependencies": { + "Microsoft.OpenApi": "1.4.3" + } + }, + "Swashbuckle.AspNetCore": { + "type": "Direct", + "requested": "[6.4.0, )", + "resolved": "6.4.0", + "contentHash": "eUBr4TW0up6oKDA5Xwkul289uqSMgY0xGN4pnbOIBqCcN9VKGGaPvHX3vWaG/hvocfGDP+MGzMA0bBBKz2fkmQ==", + "dependencies": { + "Microsoft.Extensions.ApiDescription.Server": "6.0.5", + "Swashbuckle.AspNetCore.Swagger": "6.4.0", + "Swashbuckle.AspNetCore.SwaggerGen": "6.4.0", + "Swashbuckle.AspNetCore.SwaggerUI": "6.4.0" + } + }, + "CommunityToolkit.HighPerformance": { + "type": "Transitive", + "resolved": "8.2.2", + "contentHash": "+zIp8d3sbtYaRbM6hqDs4Ui/z34j7DcUmleruZlYLE4CVxXq+MO8XJyIs42vzeTYFX+k0Iq1dEbBUnQ4z/Gnrw==" + }, + "Imageflow.AllPlatforms": { + "type": "Transitive", + "resolved": "0.13.1", + "contentHash": "cOuUD9JqwgGqkOwaXe3rjmHdA8F1x1Bqsu4m9x9tgJUGsMqytOeujYHz/trctU+VY8rODoCVw4fStJ8vVELIeQ==", + "dependencies": { + "Imageflow.NativeRuntime.osx-x86_64": "2.0.0-preview8", + "Imageflow.NativeRuntime.ubuntu-x86_64": "2.0.0-preview8", + "Imageflow.NativeRuntime.win-x86": "2.0.0-preview8", + "Imageflow.NativeRuntime.win-x86_64": "2.0.0-preview8", + "Imageflow.Net": "0.13.1" + } + }, + "Imageflow.NativeRuntime.osx-x86_64": { + "type": "Transitive", + "resolved": "2.0.0-preview8", + "contentHash": "3wEglrMqVzlnAVBTdK6qcRySdo/4ajBP5ASRuK3yHfBqPp3ld4ke6guxuSZbgDTObIxai7KTLlsIvQZhusymUA==" + }, + "Imageflow.NativeRuntime.ubuntu-x86_64": { + "type": "Transitive", + "resolved": "2.0.0-preview8", + "contentHash": "H8K5kZqcM3IliDRZD3H8BN6TbeLgcW+6FsDZ3EvlqBvu41s+Lv9vxE+c3m1cUQhsYBs76udUhgJFNR1D6x3U5g==" + }, + "Imageflow.NativeRuntime.win-x86": { + "type": "Transitive", + "resolved": "2.0.0-preview8", + "contentHash": "WunIva5NZ2iMPKCyz8ZTkN7SRaW3szBijMg5YK7jaSFZHw8Xiky/GFfghc0XgWTuILxwO4YbY86e8QvW8CBigQ==" + }, + "Imageflow.NativeRuntime.win-x86_64": { + "type": "Transitive", + "resolved": "2.0.0-preview8", + "contentHash": "1rY6C9Hjj7U9toa7FlnveiSBKccZlvCaHwdxPRQS0vDpAZZCJrTA/H7VYdreifpnIDInYcf0i/3oEKzEnj884w==" + }, + "Imageflow.Net": { + "type": "Transitive", + "resolved": "0.13.1", + "contentHash": "QHSghMGgiy4DhRloqEgNaaY+AM/28mwSF5Q371B90JyKDGIEtJPYMX+d8AkCmHuuf9Tgc6Zl8v+9ieY5yXGcNw==", + "dependencies": { + "Microsoft.IO.RecyclableMemoryStream": "[3.0.0, 4.0.0)", + "System.Text.Json": "6.0.9" + } + }, + "Microsoft.Extensions.ApiDescription.Server": { + "type": "Transitive", + "resolved": "6.0.5", + "contentHash": "Ckb5EDBUNJdFWyajfXzUIMRkhf52fHZOQuuZg/oiu8y7zDCVwD0iHhew6MnThjHmevanpxL3f5ci2TtHQEN6bw==" + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "2.2.0", + "contentHash": "65MrmXCziWaQFrI0UHkQbesrX5wTwf9XPjY5yFm/VkgJKFJ5gqvXRoXjIZcf2wLi5ZlwGz/oMYfyURVCWbM5iw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "2.2.0" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "2.2.0", + "contentHash": "f9hstgjVmr6rmrfGSpfsVOl2irKAgr1QjrSi3FgnS7kulxband50f2brRLwySAQTADPZeTdow0mpSMcoAdadCw==" + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "2.2.0", + "contentHash": "EcnaSsPTqx2MGnHrmWOD0ugbuuqVT8iICqSqPzi45V5/MA1LjUNb0kwgcxBGqizV1R+WeBK7/Gw25Jzkyk9bIw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "2.2.0" + } + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "2.2.0", + "contentHash": "+k4AEn68HOJat5gj1TWa6X28WlirNQO9sPIIeQbia+91n03esEtMSSoekSTpMjUzjqtJWQN3McVx0GvSPFHF/Q==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "2.2.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.2.0", + "Microsoft.Extensions.FileProviders.Abstractions": "2.2.0", + "Microsoft.Extensions.Logging.Abstractions": "2.2.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "2.2.0", + "contentHash": "B2WqEox8o+4KUOpL7rZPyh6qYjik8tHi2tN8Z9jZkHzED8ElYgZa/h6K+xliB435SqUcWT290Fr2aa8BtZjn8A==" + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "2.2.0", + "contentHash": "azyQtqbm4fSaDzZHD/J+V6oWMFaf2tWP4WEGIYePLCMw3+b2RQdj9ybgbQyjCshcitQKQ4lEDOZjmSlTTrHxUg==", + "dependencies": { + "System.Memory": "4.5.1", + "System.Runtime.CompilerServices.Unsafe": "4.5.1" + } + }, + "Microsoft.IO.RecyclableMemoryStream": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "irv0HuqoH8Ig5i2fO+8dmDNdFdsrO+DoQcedwIlb810qpZHBNQHZLW7C/AHBQDgLLpw2T96vmMAy/aE4Yj55Sg==" + }, + "Microsoft.OpenApi": { + "type": "Transitive", + "resolved": "1.4.3", + "contentHash": "rURwggB+QZYcSVbDr7HSdhw/FELvMlriW10OeOzjPT7pstefMo7IThhtNtDudxbXhW+lj0NfX72Ka5EDsG8x6w==" + }, + "Swashbuckle.AspNetCore.Swagger": { + "type": "Transitive", + "resolved": "6.4.0", + "contentHash": "nl4SBgGM+cmthUcpwO/w1lUjevdDHAqRvfUoe4Xp/Uvuzt9mzGUwyFCqa3ODBAcZYBiFoKvrYwz0rabslJvSmQ==", + "dependencies": { + "Microsoft.OpenApi": "1.2.3" + } + }, + "Swashbuckle.AspNetCore.SwaggerGen": { + "type": "Transitive", + "resolved": "6.4.0", + "contentHash": "lXhcUBVqKrPFAQF7e/ZeDfb5PMgE8n5t6L5B6/BQSpiwxgHzmBcx8Msu42zLYFTvR5PIqE9Q9lZvSQAcwCxJjw==", + "dependencies": { + "Swashbuckle.AspNetCore.Swagger": "6.4.0" + } + }, + "Swashbuckle.AspNetCore.SwaggerUI": { + "type": "Transitive", + "resolved": "6.4.0", + "contentHash": "1Hh3atb3pi8c+v7n4/3N80Jj8RvLOXgWxzix6w3OZhB7zBGRwsy7FWr4e3hwgPweSBpwfElqj4V4nkjYabH9nQ==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "l4zZJ1WU2hqpQQHXz1rvC3etVZN+2DLmQMO79FhOTZHMn8tDRr+WU287sbomD0BETlmKDn0ygUgVy9k5xkkJdA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "6.0.3", + "contentHash": "ryTgF+iFkpGZY1vRQhfCzX0xTdlV3pyaTTqRu2ETbEv+HlV7O6y7hyQURnghNIXvctl5DuZ//Dpks6HdL/Txgw==" + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "sDJYJpGtTgx+23Ayu5euxG5mAXWdkDb4+b0rD0Cab0M1oQS9H0HXGPriKcqpXuiJDTV7fTp/d+fMDJmnr6sNvA==" + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "Vg8eB5Tawm1IFqj4TVK1czJX89rhFxJo9ELqc/Eiq0eXy13RK00eubyU6TJE6y+GQXjyV5gSfiewDUZjQgSE0w==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "6.0.9", + "contentHash": "2j16oUgtIzl7Xtk7demG0i/v5aU/ZvULcAnJvPb63U3ZhXJ494UYcxuEj5Fs49i3XDrk5kU/8I+6l9zRCw3cJw==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encodings.Web": "6.0.0" + } + }, + "imageflow.server": { + "type": "Project", + "dependencies": { + "Imageflow.AllPlatforms": "[0.13.1, )", + "Imazen.Common": "[0.1.0--notset, )", + "Imazen.Routing": "[0.1.0--notset, )", + "Microsoft.IO.RecyclableMemoryStream": "[3.0.0, )" + } + }, + "imazen.abstractions": { + "type": "Project", + "dependencies": { + "CommunityToolkit.HighPerformance": "[8.*, )", + "Microsoft.Extensions.Hosting.Abstractions": "[2.*, )", + "System.Collections.Immutable": "[6.*, )", + "System.Text.Encodings.Web": "[6.*, )" + } + }, + "imazen.common": { + "type": "Project", + "dependencies": { + "Imazen.Abstractions": "[0.1.0--notset, )", + "Microsoft.Extensions.Hosting.Abstractions": "[2.2.0, )" + } + }, + "imazen.routing": { + "type": "Project", + "dependencies": { + "CommunityToolkit.HighPerformance": "[8.*, )", + "Imageflow.Net": "[0.13.0, )", + "Imazen.Abstractions": "[0.1.0--notset, )", + "Imazen.Common": "[0.1.0--notset, )", + "System.Collections.Immutable": "[6.*, )", + "System.IO.Pipelines": "[6.*, )" + } + } + } + } +} \ No newline at end of file diff --git a/examples/Imageflow.Server.ExampleModernAPI/wwwroot/images/fire.jpg b/examples/Imageflow.Server.ExampleModernAPI/wwwroot/images/fire.jpg new file mode 100644 index 0000000..d4bb42c Binary files /dev/null and b/examples/Imageflow.Server.ExampleModernAPI/wwwroot/images/fire.jpg differ