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