Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support FastEndpoints. Issue-72 #76

Merged
merged 2 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.5.0] - 2024-07-00
- 🌟 Support [FastEndpoints](https://fast-endpoints.com/), a developer-friendly alternative to Minimal APIs and MVC. Thank you, [@CaptainPowerTurtle](https://github.com/CaptainPowerTurtle), for reporting issue [#72](https://github.com/ikyriak/IdempotentAPI/issues/72) and [@dj-nitehawk](https://github.com/dj-nitehawk) for helping me integrate with `FastEndpoints` 🙏💪.
- IdempotentAPI.MinimalAPI `v3.1.0`:
- Configure the idempotent options by implementing the `IIdempotencyOptionsProvider` to provide the `IIdempotencyOptions` based on our needs (e.g., per endpoint). For example, we could return the `IIdempotencyOptions` based on the requested path and register `IdempotencyOptionsProvider` in the `Program.cs`. Thank you, [@JonasLeetTheWay](https://github.com/JonasLeetTheWay) for reporting issue [#73](https://github.com/ikyriak/IdempotentAPI/issues/73).
- Configure the idempotent options by implementing the `IIdempotencyOptionsProvider` to provide the `IIdempotencyOptions` based on our needs (e.g., per endpoint). For example, we could return the `IIdempotencyOptions` based on the requested path and register `IdempotencyOptionsProvider` in the `Program.cs`. Thank you, [@JonasLeetTheWay](https://github.com/JonasLeetTheWay) for reporting issue [#73](https://github.com/ikyriak/IdempotentAPI/issues/73) 🙏.
```c#
// Program.cs
builder.Services.AddIdempotentMinimalAPI(new IdempotencyOptionsProvider());
Expand All @@ -28,7 +29,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
}
}
```

- ...
-

Expand Down
9 changes: 8 additions & 1 deletion IdempotentAPI.sln
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IdempotentAPI.TestWebMinimalAPIs", "tests\IdempotentAPI.TestWebMinimalAPIs\IdempotentAPI.TestWebMinimalAPIs.csproj", "{5DA6F50E-F41A-43BB-A584-82C6B49A24B3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdempotentAPI.MinimalAPI", "src\IdempotentAPI.MinimalAPI\IdempotentAPI.MinimalAPI.csproj", "{165B191F-F219-467C-844B-D470228D6DA3}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IdempotentAPI.MinimalAPI", "src\IdempotentAPI.MinimalAPI\IdempotentAPI.MinimalAPI.csproj", "{165B191F-F219-467C-844B-D470228D6DA3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdempotentAPI.TestFastEndpointsAPIs", "tests\IdempotentAPI.TestFastEndpointsAPIs\IdempotentAPI.TestFastEndpointsAPIs.csproj", "{51BD5A49-2B24-4F22-AAFE-92E7EA9D2FAE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -104,6 +106,10 @@ Global
{165B191F-F219-467C-844B-D470228D6DA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{165B191F-F219-467C-844B-D470228D6DA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{165B191F-F219-467C-844B-D470228D6DA3}.Release|Any CPU.Build.0 = Release|Any CPU
{51BD5A49-2B24-4F22-AAFE-92E7EA9D2FAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{51BD5A49-2B24-4F22-AAFE-92E7EA9D2FAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{51BD5A49-2B24-4F22-AAFE-92E7EA9D2FAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{51BD5A49-2B24-4F22-AAFE-92E7EA9D2FAE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -123,6 +129,7 @@ Global
{568F2BE6-17A9-451E-90E8-BE1F727BEC35} = {9221FD4F-D7F9-40D1-AD36-D59B1C26C487}
{5DA6F50E-F41A-43BB-A584-82C6B49A24B3} = {B0CA0D1D-3C22-4935-8467-310509EDB755}
{165B191F-F219-467C-844B-D470228D6DA3} = {DD1EA88C-81B6-47D8-B8B6-58AC2DE1BFE7}
{51BD5A49-2B24-4F22-AAFE-92E7EA9D2FAE} = {B0CA0D1D-3C22-4935-8467-310509EDB755}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0865C8C6-957E-4E70-A784-8EBC2282E4AB}
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ The following figure shows a simplified example of the `IdempotentAPI` library f
- [samcook/RedLock.net](https://github.com/samcook/RedLock.net): Supports the [Redis Redlock](https://redis.io/docs/reference/patterns/distributed-locks/) algorithm.
- [madelson/DistributedLock](https://github.com/madelson/DistributedLock): Supports multiple technologies such as Redis, SqlServer, Postgres and many [more](https://github.com/madelson/DistributedLock#implementations).
- 💪**Powerful**: Can be used in high-load scenarios.
- ✳ **NEW** ✳ - ✅ Supports Minimal APIs.
- 🌟 Supports Minimal APIs.
- ✳ **NEW** ✳ - 🌟 Support [FastEndpoints](https://fast-endpoints.com/), a developer-friendly alternative to Minimal APIs and MVC.



Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public IdempotentAPIEndpointFilter(IServiceProvider serviceProvider)
var filters = new List<IFilterMetadata>();
var actionArguments = new Dictionary<string, object?>();

idempotency.PrepareMinimalApiIdempotency(context.HttpContext, context.Arguments);
await idempotency.PrepareMinimalApiIdempotencyAsync(context.HttpContext, context.Arguments);

var actionExecutingContext = new ActionExecutingContext(actionContext, filters, actionArguments, null!);

Expand Down
55 changes: 47 additions & 8 deletions src/IdempotentAPI/Core/Idempotency.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#nullable enable
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand All @@ -23,6 +24,8 @@ namespace IdempotentAPI.Core
{
public class Idempotency
{
private const string FastEndpointsResponseItemKey = "FastEndpointsResponse";
private const string FastEndpointsEndpointFactory = "FastEndpoints.EndpointFactory";
private readonly object _cacheEntryOptions;
private readonly bool _cacheOnlySuccessResponses;
private readonly IIdempotencyAccessCache _distributedCache;
Expand Down Expand Up @@ -161,12 +164,12 @@ public async Task PrepareIdempotency(ResourceExecutingContext context)
return;
}

string requestsDataHash = await GenerateRequestsDataHash(context.HttpContext.Request);
string requestsDataHash = await GenerateRequestsDataHashAsync(context.HttpContext.Request);

context.HttpContext.SetRequestsDataHash(requestsDataHash);
}

public void PrepareMinimalApiIdempotency(HttpContext httpContext, IList<object?> arguments)
public async Task PrepareMinimalApiIdempotencyAsync(HttpContext httpContext, IList<object?> arguments)
{
// Check if Idempotency can be applied:
if (!CanPerformIdempotency(httpContext.Request))
Expand All @@ -184,7 +187,7 @@ and not HttpResponse
and not ClaimsPrincipal
and not CancellationToken);

string requestsDataHash = GenerateRequestsDataHashMinimalApi(filteredArguments, httpContext.Request);
string requestsDataHash = await GenerateRequestsDataHashMinimalApiAsync(filteredArguments, httpContext.Request);

httpContext.SetRequestsDataHash(requestsDataHash);
}
Expand Down Expand Up @@ -409,11 +412,11 @@ private byte[] GenerateCacheData(ResultExecutingContext context)
cacheData.Add("Response.StatusCode", context.HttpContext.Response.StatusCode);
cacheData.Add("Response.ContentType", context.HttpContext.Response.ContentType);

Dictionary<string, List<string>> Headers = context.HttpContext.Response.Headers
Dictionary<string, List<string>> headers = context.HttpContext.Response.Headers
.Where(h => !_excludeHttpHeaderKeys.Contains(h.Key))
.ToDictionary(h => h.Key, h => h.Value.ToList());

cacheData.Add("Response.Headers", Headers);
cacheData.Add("Response.Headers", headers);


// 2019-07-05: Response.Body cannot be accessed because its not yet created.
Expand All @@ -422,7 +425,11 @@ private byte[] GenerateCacheData(ResultExecutingContext context)
var contextResult = context.Result;
resultObjects.Add("ResultType", contextResult.GetType().AssemblyQualifiedName);

if (contextResult is CreatedAtRouteResult route)
if (context.HttpContext.Items.ContainsKey(FastEndpointsResponseItemKey))
{
resultObjects.Add("ResultValue", context.HttpContext.Items[FastEndpointsResponseItemKey]);
}
else if (contextResult is CreatedAtRouteResult route)
{
//CreatedAtRouteResult.CreatedAtRouteResult(string routeName, object routeValues, object value)
resultObjects.Add("ResultValue", route.Value);
Expand Down Expand Up @@ -476,7 +483,7 @@ private byte[] GenerateRequestInFlightCacheData(Guid guid)
return serializedCacheData;
}

private string GenerateRequestsDataHashMinimalApi(IEnumerable<object?> arguments, HttpRequest httpRequest)
private async Task<string> GenerateRequestsDataHashMinimalApiAsync(IEnumerable<object?> arguments, HttpRequest httpRequest)
{
List<object?> requestsData = new(arguments);

Expand All @@ -486,10 +493,42 @@ private string GenerateRequestsDataHashMinimalApi(IEnumerable<object?> arguments
requestsData.Add(httpRequest.Path.ToString());
}

// Read the post data from the request body
if (arguments.Any(a => a?.GetType().ToString() == FastEndpointsEndpointFactory))
{
string requestBodyHash = await GenerateRequestsBodyHashAsync(httpRequest);
requestsData.Add(requestBodyHash);
}

return Utils.GetHash(_hashAlgorithm, JsonConvert.SerializeObject(requestsData));
}

private async Task<string> GenerateRequestsDataHash(HttpRequest httpRequest)
private async Task<string> GenerateRequestsBodyHashAsync(HttpRequest httpRequest)
{
httpRequest.EnableBuffering();
httpRequest.Body.Position = 0;

var buffer = ArrayPool<byte>.Shared.Rent(4096);
int bytesRead;

string requestBodyHash;
using (var ms = new MemoryStream())
{
while ((bytesRead = await httpRequest.Body.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
ms.Write(buffer, 0, bytesRead);
}

requestBodyHash = Utils.GetHash(_hashAlgorithm, ms.ToArray());
}

ArrayPool<byte>.Shared.Return(buffer);

httpRequest.Body.Position = 0;
return requestBodyHash;
}

private async Task<string> GenerateRequestsDataHashAsync(HttpRequest httpRequest)
{
List<object> requestsData = new();

Expand Down
7 changes: 6 additions & 1 deletion src/IdempotentAPI/Helpers/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@ public static class Utils
private static readonly JsonSerializerSettings _jsonSerializerSettingsAuto = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto };

public static string GetHash(HashAlgorithm hashAlgorithm, string input)
{
return GetHash(hashAlgorithm, Encoding.UTF8.GetBytes(input));
}

public static string GetHash(HashAlgorithm hashAlgorithm, byte[] input)
{
// Convert the input string to a byte array and compute the hash.
byte[] data = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(input));
byte[] data = hashAlgorithm.ComputeHash(input);

// Create a new Stringbuilder to collect the bytes
// and create a string.
Expand Down
2 changes: 1 addition & 1 deletion src/IdempotentAPI/IdempotentAPI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<Authors>Ioannis Kyriakidis</Authors>
<Company>ikyriakidis.com</Company>
<Version>2.4.0</Version>
<Version>2.5.0</Version>
<Description>IdempotentAPI is an ASP.NET Core attribute by which any HTTP write operations (POST and PATCH) can have effect only once for the given request data.</Description>
<PackageTags>idempotent api;idempotency;asp.net core;attribute;middleware;rest</PackageTags>
<RepositoryUrl>https://github.com/ikyriak/IdempotentAPI.git</RepositoryUrl>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,13 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\IdempotentAPI.TestFastEndpointsAPIs\IdempotentAPI.TestFastEndpointsAPIs.csproj">
<Aliases>TestFastEndpointsAPIs</Aliases>
</ProjectReference>
<ProjectReference Include="..\IdempotentAPI.TestWebAPIs\IdempotentAPI.TestWebAPIs.csproj" />
<ProjectReference Include="..\IdempotentAPI.TestWebMinimalAPIs\IdempotentAPI.TestWebMinimalAPIs.csproj" />
<ProjectReference Include="..\IdempotentAPI.TestWebMinimalAPIs\IdempotentAPI.TestWebMinimalAPIs.csproj">
<Aliases>TestWebMinimalAPIs</Aliases>
</ProjectReference>
</ItemGroup>

</Project>
Loading