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

Add UseIdempotencyOption #70

Merged
merged 4 commits into from
Apr 3, 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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@ 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.4.0] - 2024-04-03
- Add an extension to register the `IIdempotencyOptions` that will enable the use of the `[Idempotent(UseIdempotencyOption = true)]` option. In this way, the attribute will use the predefined `IIdempotencyOptions`.
```c#
// Register the Core service and the `IIdempotencyOptions`.
services.AddIdempotentAPI(idempotencyOptions);
```

```c#
// To use the `IIdempotencyOptions`, set the `UseIdempotencyOption` property to `true`.
[HttpPost()]
[Idempotent(UseIdempotencyOption = true)]
public ActionResult AddMyEntity()
{
// ...
}
```

## [2.3.0] - 2024-03-07 - Minimal APIs (IdempotentAPI.MinimalAPI v3.0.0)
- 🌟 The `AddIdempotentMinimalAPI(...)` extension is introduced to simplify the `IdempotentAPI.MinimalAPI` registration with DI improvements by [@hartmark](https://github.com/hartmark).
- ❗ IMPORTANT: To use the new extensions, the **BREAKING** `IdempotentAPI.MinimalAPI v3.0.0` should be used.
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Idempotent API (v2.3.0)
# Idempotent API (v2.4.0)



Expand Down Expand Up @@ -121,14 +121,20 @@ The following figure shows a simplified example of the `IdempotentAPI` library f
Let's see how we could use the NuGet packages in a Web API project. For more examples and code, you can check the [sample projects](https://github.com/ikyriak/IdempotentAPI/tree/master/samples). The `IdempotentAPI` can be installed via the NuGet UI or the NuGet package manager console:

```powershell
PM> Install-Package IdempotentAPI -Version 2.3.0
PM> Install-Package IdempotentAPI -Version 2.4.0
```

and, register the **IdempotentAPI Core services** for either controller-based APIs or minimal APIs.

```c#
// For Controller-Based APIs:
services.AddIdempotentAPI();

// OR

// For Controller-Based APIs: Register the `IIdempotencyOptions` that will enable the use of the `[Idempotent(UseIdempotencyOption = true)]` option.
services.AddIdempotentAPI(idempotencyOptions);

```

OR
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace IdempotentAPI.Extensions.DependencyInjection
public static class IdempotentAPIExtensions
{
/// <summary>
/// Register the Core services that are required by the IdempotentAPI. Currently, it only registers the service to access the cache (<see cref="IIdempotencyAccessCache"/>).
/// Register the Core service that is required by the IdempotentAPI (<see cref="IIdempotencyAccessCache"/>).
/// </summary>
/// <param name="serviceCollection"></param>
/// <returns></returns>
Expand All @@ -20,10 +20,25 @@ public static IServiceCollection AddIdempotentAPI(this IServiceCollection servic
return serviceCollection;
}

/// <summary>
/// Register the Core service that is required by the IdempotentAPI (<see cref="IIdempotencyAccessCache"/>) and register the <see cref="IIdempotencyOptions"/> that will enable the use of the <see cref="Filters.IdempotentAttribute.UseIdempotencyOption"/>.
/// </summary>
/// <param name="serviceCollection"></param>
/// <param name="idempotencyOptions">It will enable the use of the <see cref="Filters.IdempotentAttribute.UseIdempotencyOption"/>. So, afterward, you could use the: <code>[Idempotent(UseIdempotencyOption = true)]</code></param>
/// <returns></returns>
public static IServiceCollection AddIdempotentAPI(this IServiceCollection serviceCollection, IdempotencyOptions idempotencyOptions)
{
serviceCollection.AddSingleton<IIdempotencyAccessCache, IdempotencyAccessCache>();
serviceCollection.AddSingleton<IIdempotencyOptions>(idempotencyOptions);

return serviceCollection;
}

/// <summary>
/// Register the Core services that are required by the IdempotentAPI for Minimal APIs.
/// </summary>
/// <param name="serviceCollection"></param>
/// <param name="idempotencyOptions"></param>
/// <returns></returns>
public static IServiceCollection AddIdempotentMinimalAPI(this IServiceCollection serviceCollection, IdempotencyOptions idempotencyOptions)
{
Expand Down
24 changes: 16 additions & 8 deletions src/IdempotentAPI/Filters/IdempotencyAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using IdempotentAPI.AccessCache;
using IdempotentAPI.Core;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace IdempotentAPI.Filters
Expand Down Expand Up @@ -47,25 +48,32 @@ public double ExpiresInMilliseconds
///<inheritdoc/>
public bool IsIdempotencyOptional { get; set; } = DefaultIdempotencyOptions.IsIdempotencyOptional;

/// <summary>
/// By default, idempotency settings are taken from the attribute properties.
/// When this flag is set to true, the settings will be taken from the registered <see cref="IIdempotencyOptions"/> in the ServiceCollection
/// </summary>
public bool UseIdempotencyOption { get; set; } = false;

public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
var distributedCache = (IIdempotencyAccessCache)serviceProvider.GetService(typeof(IIdempotencyAccessCache));
var loggerFactory = (ILoggerFactory)serviceProvider.GetService(typeof(ILoggerFactory));
var idempotencyOptions = UseIdempotencyOption ? serviceProvider.GetRequiredService<IIdempotencyOptions>() : this;

TimeSpan? distributedLockTimeout = DistributedLockTimeoutMilli >= 0
? TimeSpan.FromMilliseconds(DistributedLockTimeoutMilli)
TimeSpan? distributedLockTimeout = idempotencyOptions.DistributedLockTimeoutMilli >= 0
? TimeSpan.FromMilliseconds(idempotencyOptions.DistributedLockTimeoutMilli)
: null;

return new IdempotencyAttributeFilter(
distributedCache,
loggerFactory,
Enabled,
ExpiresInMilliseconds,
HeaderKeyName,
DistributedCacheKeysPrefix,
idempotencyOptions.ExpiresInMilliseconds,
idempotencyOptions.HeaderKeyName,
idempotencyOptions.DistributedCacheKeysPrefix,
distributedLockTimeout,
CacheOnlySuccessResponses,
IsIdempotencyOptional);
idempotencyOptions.CacheOnlySuccessResponses,
idempotencyOptions.IsIdempotencyOptional);
}
}
}
}
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.3.0</Version>
<Version>2.4.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
27 changes: 27 additions & 0 deletions tests/IdempotentAPI.IntegrationTests/SingleApiTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,33 @@ public async Task PostTest_ShouldReturnCachedResponse(int httpClientIndex)
content1.Should().Be(content2);
}


[Fact]
public async Task PostTest_WhenUsingIdempotencyOptionOnWebApiClient_ShouldReturnCachedResponse()
{
// Arrange
var guid = Guid.NewGuid().ToString();

int httpClientIndex = WebApiClientIndex;
_httpClients[httpClientIndex].DefaultRequestHeaders.Clear();
_httpClients[httpClientIndex].DefaultRequestHeaders.Add("IdempotencyKey", guid);

// Act
var response1 = await _httpClients[httpClientIndex].PostAsync("v6/TestingIdempotentAPIPerMethod/testUseIdempotencyOption", null);
var response2 = await _httpClients[httpClientIndex].PostAsync("v6/TestingIdempotentAPIPerMethod/testUseIdempotencyOption", null);

// Assert
var content1 = await response1.Content.ReadAsStringAsync();
var content2 = await response2.Content.ReadAsStringAsync();
_testOutputHelper.WriteLine($"content1: {Environment.NewLine}{content1}");
_testOutputHelper.WriteLine($"content2: {Environment.NewLine}{content2}");

response1.StatusCode.Should().Be(HttpStatusCode.OK, content1);
response2.StatusCode.Should().Be(HttpStatusCode.OK, content2);

content1.Should().Be(content2);
}

[Theory]
[InlineData(WebApiClientIndex)]
[InlineData(WebMinimalApiClientIndex)]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using IdempotentAPI.Filters;
using IdempotentAPI.TestWebAPIs.DTOs;
using Microsoft.AspNetCore.Mvc;

namespace IdempotentAPI.TestWebAPIs.Controllers
{
[ApiController]
[ApiVersion("6.0")]
[Route("v{version:apiVersion}/[controller]")]

[Consumes("application/json")]
[Produces("application/json")]

public class TestingIdempotentAPIPerMethodController : ControllerBase
{
private readonly ILogger<TestingIdempotentAPIController> _logger;

public TestingIdempotentAPIPerMethodController(ILogger<TestingIdempotentAPIController> logger)
{
_logger = logger;
}

[HttpPost("testUseIdempotencyOption")]
[Idempotent(UseIdempotencyOption = true)]
public ActionResult TestUseIdempotencyOption()
{
return Ok(new ResponseDTOs());
}
}
}
8 changes: 7 additions & 1 deletion tests/IdempotentAPI.TestWebAPIs/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Net;
using IdempotentAPI.Cache.DistributedCache.Extensions.DependencyInjection;
using IdempotentAPI.Cache.FusionCache.Extensions.DependencyInjection;
using IdempotentAPI.Core;
using IdempotentAPI.DistributedAccessLock.MadelsonDistributedLock.Extensions.DependencyInjection;
using IdempotentAPI.DistributedAccessLock.RedLockNet.Extensions.DependencyInjection;
using IdempotentAPI.Extensions.DependencyInjection;
Expand Down Expand Up @@ -33,9 +34,14 @@ public virtual void ConfigureServices(IServiceCollection services)
// Add services to the container.
services.AddApiVersioningConfigured();

IdempotencyOptions idempotencyOptions = new()
{
CacheOnlySuccessResponses = true,
DistributedLockTimeoutMilli = 2000,
};

// Register the IdempotentAPI Core services.
services.AddIdempotentAPI();
services.AddIdempotentAPI(idempotencyOptions);

services.AddSwaggerGen(x =>
x.SwaggerDoc("v6", new OpenApiInfo { Title = "IdempotentAPI.TestWebAPIs1 - Swagger", Version = "v6" }));
Expand Down
Loading