diff --git a/Directory.Build.props b/Directory.Build.props index 40212b0..caa3aad 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,6 +18,6 @@ MIT True - 0.6.5 + 0.6.6 \ No newline at end of file diff --git a/README.md b/README.md index 3a4439b..686d7e0 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ app.Run(); A [MinimalEndpoint](#minimalendpoint) is the most straighforward way to define a Minimal Api in REPR format. -Configuration of each endpoint implementation starts with calling one of the MapGet, MapPost, MapPut, MapDelete and MapPatch methods with a route pattern string. The return from any of these methods, a RouteHandlerBuilder instance, can be used to further customize the endpoint like a regular Minimal Api. +Configuration of each endpoint implementation starts with calling one of the MapGet, MapPost, MapPut, MapDelete and MapPatch methods with a route pattern string. The return from any of these methods, a RouteHandlerBuilder instance, can be used to further customize the endpoint similar to a Minimal Api. The request is processed in 'HandleAsync' method. Request is passed to handler method as parameter after validation (if a validator is registered for request model). Handler method returns a response model or a string or a Minimal Api IResult based response. @@ -250,10 +250,10 @@ internal class UploadBook ### Route groups -By default, all endpoints are mapped under root route group. It is possible to define route groups similar to using 'MapGroup' extension method used to map Minimal Apis under a group. Since endpoints are configured by endpoint basis in the 'Configure' method of each endpoint, the approach is a little different than regular Minimal Apis, but these are still Minimal Api route groups and can be configured by any extension method of RouteGroupBuilder. Route groups are also subject to auto discovery and registration, similar to endpoints. +By default, all endpoints are mapped under root route group. It is possible to define route groups similar to using 'MapGroup' extension method used to map Minimal Apis under a group. Since endpoints are configured by endpoint basis in the 'Configure' method of each endpoint, the approach is a little different than configuring a Minimal Api. But route group configuration still utilize Minimal Api route groups and can be decorated by any extension method of RouteGroupBuilder. Route groups are also subject to auto discovery and registration, similar to endpoints. - [Create a route group implementation](./samples/ShowcaseWebApi/Features/FeaturesRouteGroup.cs) by inheriting RouteGroupConfigurator and implementing 'Configure' method, -- Configuration of each route group implementation starts with calling MapGroup method with a route pattern prefix. The return of 'MapGroup' method, a RouteGroupBuilder instance, can be used to further customize the route group like a regular Minimal Api route group. +- Configuration of each route group implementation starts with calling MapGroup method with a route pattern prefix. The return of 'MapGroup' method, a RouteGroupBuilder instance, can be used to further customize the route group like any Minimal Api route group. - Apply MapToGroup attribute to either other [route group](./samples/ShowcaseWebApi/Features/Books/Configuration/BooksV1RouteGroup.cs) or [endpoint](./samples/ShowcaseWebApi/Features/Books/CreateBook.cs) classes that will be mapped under created route group. Use type of the new route group implementation as GroupType parameter to the attribute. Following sample creates a parent route group (FeaturesRouteGroup), a child route group under it (BooksV1RouteGroup) and maps an endpoint (CreateBook) to child route group. Group configuration methods used for this particular sample are all part of Minimal Apis ecosystem and are under [Asp.Versioning](https://github.com/dotnet/aspnet-api-versioning). @@ -347,13 +347,13 @@ internal class DisabledCustomerFeature ## Performance -WebResultEndpoints have a slight overhead (3-4%) over regular Minimal Apis on request/sec metric under load tests with 100 virtual users. - -MinimalEndpoints perform about same as regular Minimal Apis. +Under load tests with 100 virtual users: +- MinimalEndpoints perform nearly the same (~1%) as Minimal Apis, +- WebResultEndpoints introduce a slight overhead (~2%) compared to Minimal Apis in terms of requests per second. The web apis called for tests, perform only in-process operations like resolving dependency, validating input, calling local methods with no network or disk I/O. -See [test results](./samples/BenchmarkWebApi/BenchmarkFiles/Results/0.6.5/inprocess_benchmark_results.txt) under [BenchmarkFiles](https://github.com/modabas/ModEndpoints/tree/main/samples/BenchmarkWebApi/BenchmarkFiles) folder of BenchmarkWebApi project for detailed results and test scripts. +See [test results](./samples/BenchmarkWebApi/BenchmarkFiles/Results/0.6.6/inprocess_benchmark_results.txt) under [BenchmarkFiles](https://github.com/modabas/ModEndpoints/tree/main/samples/BenchmarkWebApi/BenchmarkFiles) folder of BenchmarkWebApi project for detailed results and test scripts. ## Endpoint Types diff --git a/samples/BenchmarkWebApi/BenchmarkFiles/Results/0.6.6/basic_benchmark_results.txt b/samples/BenchmarkWebApi/BenchmarkFiles/Results/0.6.6/basic_benchmark_results.txt new file mode 100644 index 0000000..8de89cc --- /dev/null +++ b/samples/BenchmarkWebApi/BenchmarkFiles/Results/0.6.6/basic_benchmark_results.txt @@ -0,0 +1,121 @@ + k6  .\k6 run minimal_api_basic.js + + /\ Grafana /‾‾/ + /\ / \ |\ __ / / + / \/ \ | |/ / / ‾‾\ + / \ | ( | (‾) | + / __________ \ |_|\_\ \_____/ + + execution: local + script: minimal_api_basic.js + output: - + + scenarios: (100.00%) 1 scenario, 100 max VUs, 2m20s max duration (incl. graceful stop): + * default: Up to 100 looping VUs for 1m50s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s) + + + ✓ status was 200 + + checks.........................: 100.00% 8333889 out of 8333889 + data_received..................: 1.4 GB 13 MB/s + data_sent......................: 850 MB 7.7 MB/s + http_req_blocked...............: avg=3.33µs min=0s med=0s max=49.27ms p(90)=0s p(95)=0s + http_req_connecting............: avg=8ns min=0s med=0s max=4.52ms p(90)=0s p(95)=0s + ✓ http_req_duration..............: avg=903.53µs min=0s med=999.7µs max=80.18ms p(90)=1.88ms p(95)=2ms + { expected_response:true }...: avg=903.53µs min=0s med=999.7µs max=80.18ms p(90)=1.88ms p(95)=2ms + http_req_failed................: 0.00% 0 out of 8333889 + http_req_receiving.............: avg=32.73µs min=0s med=0s max=74.14ms p(90)=0s p(95)=0s + http_req_sending...............: avg=10.45µs min=0s med=0s max=60.82ms p(90)=0s p(95)=0s + http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s + http_req_waiting...............: avg=860.33µs min=0s med=999.5µs max=76.87ms p(90)=1.72ms p(95)=2ms + http_reqs......................: 8333889 75762.281659/s + iteration_duration.............: avg=1ms min=0s med=1ms max=84.13ms p(90)=1.99ms p(95)=2.01ms + iterations.....................: 8333889 75762.281659/s + vus............................: 1 min=1 max=100 + vus_max........................: 100 min=100 max=100 + + +running (1m50.0s), 000/100 VUs, 8333889 complete and 0 interrupted iterations +default ✓ [======================================] 000/100 VUs 1m50s + + + k6  .\k6 run minimal_endpoint_basic.js + + /\ Grafana /‾‾/ + /\ / \ |\ __ / / + / \/ \ | |/ / / ‾‾\ + / \ | ( | (‾) | + / __________ \ |_|\_\ \_____/ + + execution: local + script: minimal_endpoint_basic.js + output: - + + scenarios: (100.00%) 1 scenario, 100 max VUs, 2m20s max duration (incl. graceful stop): + * default: Up to 100 looping VUs for 1m50s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s) + + + ✓ status was 200 + + checks.........................: 100.00% 8318368 out of 8318368 + data_received..................: 1.4 GB 13 MB/s + data_sent......................: 890 MB 8.1 MB/s + http_req_blocked...............: avg=3.26µs min=0s med=0s max=43.68ms p(90)=0s p(95)=0s + http_req_connecting............: avg=8ns min=0s med=0s max=2.99ms p(90)=0s p(95)=0s + ✓ http_req_duration..............: avg=891µs min=0s med=999.5µs max=97.17ms p(90)=1.82ms p(95)=2ms + { expected_response:true }...: avg=891µs min=0s med=999.5µs max=97.17ms p(90)=1.82ms p(95)=2ms + http_req_failed................: 0.00% 0 out of 8318368 + http_req_receiving.............: avg=32.59µs min=0s med=0s max=71.43ms p(90)=0s p(95)=0s + http_req_sending...............: avg=10.52µs min=0s med=0s max=48.81ms p(90)=0s p(95)=0s + http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s + http_req_waiting...............: avg=847.88µs min=0s med=999.2µs max=97.17ms p(90)=1.69ms p(95)=2ms + http_reqs......................: 8318368 75621.204645/s + iteration_duration.............: avg=1ms min=0s med=1ms max=142.24ms p(90)=1.99ms p(95)=2.03ms + iterations.....................: 8318368 75621.204645/s + vus............................: 1 min=1 max=100 + vus_max........................: 100 min=100 max=100 + + +running (1m50.0s), 000/100 VUs, 8318368 complete and 0 interrupted iterations +default ✓ [======================================] 000/100 VUs 1m50s + + + k6  .\k6 run webresult_endpoint_basic.js + + /\ Grafana /‾‾/ + /\ / \ |\ __ / / + / \/ \ | |/ / / ‾‾\ + / \ | ( | (‾) | + / __________ \ |_|\_\ \_____/ + + execution: local + script: webresult_endpoint_basic.js + output: - + + scenarios: (100.00%) 1 scenario, 100 max VUs, 2m20s max duration (incl. graceful stop): + * default: Up to 100 looping VUs for 1m50s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s) + + + ✓ status was 200 + + checks.........................: 100.00% 8190023 out of 8190023 + data_received..................: 1.4 GB 13 MB/s + data_sent......................: 893 MB 8.1 MB/s + http_req_blocked...............: avg=3.3µs min=0s med=0s max=40.88ms p(90)=0s p(95)=0s + http_req_connecting............: avg=9ns min=0s med=0s max=2.99ms p(90)=0s p(95)=0s + ✓ http_req_duration..............: avg=920.69µs min=0s med=999.8µs max=77.18ms p(90)=1.96ms p(95)=2.01ms + { expected_response:true }...: avg=920.69µs min=0s med=999.8µs max=77.18ms p(90)=1.96ms p(95)=2.01ms + http_req_failed................: 0.00% 0 out of 8190023 + http_req_receiving.............: avg=32.73µs min=0s med=0s max=57.97ms p(90)=0s p(95)=0s + http_req_sending...............: avg=10.55µs min=0s med=0s max=46.61ms p(90)=0s p(95)=0s + http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s + http_req_waiting...............: avg=877.41µs min=0s med=999.6µs max=66.38ms p(90)=1.79ms p(95)=2ms + http_reqs......................: 8190023 74454.706015/s + iteration_duration.............: avg=1.02ms min=0s med=1ms max=89.37ms p(90)=1.99ms p(95)=2.02ms + iterations.....................: 8190023 74454.706015/s + vus............................: 1 min=1 max=100 + vus_max........................: 100 min=100 max=100 + + +running (1m50.0s), 000/100 VUs, 8190023 complete and 0 interrupted iterations +default ✓ [======================================] 000/100 VUs 1m50s \ No newline at end of file diff --git a/samples/BenchmarkWebApi/BenchmarkFiles/Results/0.6.6/inprocess_benchmark_results.txt b/samples/BenchmarkWebApi/BenchmarkFiles/Results/0.6.6/inprocess_benchmark_results.txt new file mode 100644 index 0000000..4290399 --- /dev/null +++ b/samples/BenchmarkWebApi/BenchmarkFiles/Results/0.6.6/inprocess_benchmark_results.txt @@ -0,0 +1,121 @@ + k6  .\k6 run minimal_api_inprocess.js + + /\ Grafana /‾‾/ + /\ / \ |\ __ / / + / \/ \ | |/ / / ‾‾\ + / \ | ( | (‾) | + / __________ \ |_|\_\ \_____/ + + execution: local + script: minimal_api_inprocess.js + output: - + + scenarios: (100.00%) 1 scenario, 100 max VUs, 2m20s max duration (incl. graceful stop): + * default: Up to 100 looping VUs for 1m50s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s) + + + ✓ status was 200 + + checks.........................: 100.00% 5854352 out of 5854352 + data_received..................: 1.1 GB 10 MB/s + data_sent......................: 1.4 GB 13 MB/s + http_req_blocked...............: avg=4.49µs min=0s med=0s max=74.36ms p(90)=0s p(95)=0s + http_req_connecting............: avg=12ns min=0s med=0s max=2.04ms p(90)=0s p(95)=0s + ✓ http_req_duration..............: avg=1.26ms min=0s med=1.01ms max=131.02ms p(90)=2.05ms p(95)=2.52ms + { expected_response:true }...: avg=1.26ms min=0s med=1.01ms max=131.02ms p(90)=2.05ms p(95)=2.52ms + http_req_failed................: 0.00% 0 out of 5854352 + http_req_receiving.............: avg=51.74µs min=0s med=0s max=123.76ms p(90)=0s p(95)=0s + http_req_sending...............: avg=20.94µs min=0s med=0s max=77.71ms p(90)=0s p(95)=0s + http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s + http_req_waiting...............: avg=1.19ms min=0s med=1ms max=93.46ms p(90)=2.01ms p(95)=2.52ms + http_reqs......................: 5854352 53221.183545/s + iteration_duration.............: avg=1.43ms min=0s med=1.01ms max=132.03ms p(90)=2.28ms p(95)=2.85ms + iterations.....................: 5854352 53221.183545/s + vus............................: 1 min=1 max=100 + vus_max........................: 100 min=100 max=100 + + +running (1m50.0s), 000/100 VUs, 5854352 complete and 0 interrupted iterations +default ✓ [======================================] 000/100 VUs 1m50s + + + k6  .\k6 run minimal_endpoint_inprocess.js + + /\ Grafana /‾‾/ + /\ / \ |\ __ / / + / \/ \ | |/ / / ‾‾\ + / \ | ( | (‾) | + / __________ \ |_|\_\ \_____/ + + execution: local + script: minimal_endpoint_inprocess.js + output: - + + scenarios: (100.00%) 1 scenario, 100 max VUs, 2m20s max duration (incl. graceful stop): + * default: Up to 100 looping VUs for 1m50s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s) + + + ✓ status was 200 + + checks.........................: 100.00% 5803262 out of 5803262 + data_received..................: 1.1 GB 9.9 MB/s + data_sent......................: 1.4 GB 13 MB/s + http_req_blocked...............: avg=4.32µs min=0s med=0s max=39.88ms p(90)=0s p(95)=0s + http_req_connecting............: avg=14ns min=0s med=0s max=9.78ms p(90)=0s p(95)=0s + ✓ http_req_duration..............: avg=1.27ms min=0s med=1.01ms max=84.26ms p(90)=2.07ms p(95)=2.52ms + { expected_response:true }...: avg=1.27ms min=0s med=1.01ms max=84.26ms p(90)=2.07ms p(95)=2.52ms + http_req_failed................: 0.00% 0 out of 5803262 + http_req_receiving.............: avg=47.14µs min=0s med=0s max=82.26ms p(90)=0s p(95)=0s + http_req_sending...............: avg=19.79µs min=0s med=0s max=69.84ms p(90)=0s p(95)=0s + http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s + http_req_waiting...............: avg=1.21ms min=0s med=1.01ms max=83.26ms p(90)=2.03ms p(95)=2.52ms + http_reqs......................: 5803262 52756.420044/s + iteration_duration.............: avg=1.44ms min=0s med=1.02ms max=84.26ms p(90)=2.31ms p(95)=2.83ms + iterations.....................: 5803262 52756.420044/s + vus............................: 1 min=1 max=100 + vus_max........................: 100 min=100 max=100 + + +running (1m50.0s), 000/100 VUs, 5803262 complete and 0 interrupted iterations +default ✓ [======================================] 000/100 VUs 1m50s + + + k6  .\k6 run webresult_endpoint_inprocess.js + + /\ Grafana /‾‾/ + /\ / \ |\ __ / / + / \/ \ | |/ / / ‾‾\ + / \ | ( | (‾) | + / __________ \ |_|\_\ \_____/ + + execution: local + script: webresult_endpoint_inprocess.js + output: - + + scenarios: (100.00%) 1 scenario, 100 max VUs, 2m20s max duration (incl. graceful stop): + * default: Up to 100 looping VUs for 1m50s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s) + + + ✓ status was 200 + + checks.........................: 100.00% 5767303 out of 5767303 + data_received..................: 1.1 GB 9.8 MB/s + data_sent......................: 1.4 GB 13 MB/s + http_req_blocked...............: avg=4.44µs min=0s med=0s max=53.62ms p(90)=0s p(95)=0s + http_req_connecting............: avg=14ns min=0s med=0s max=6.82ms p(90)=0s p(95)=0s + ✓ http_req_duration..............: avg=1.29ms min=0s med=1.01ms max=91.78ms p(90)=2.1ms p(95)=2.53ms + { expected_response:true }...: avg=1.29ms min=0s med=1.01ms max=91.78ms p(90)=2.1ms p(95)=2.53ms + http_req_failed................: 0.00% 0 out of 5767303 + http_req_receiving.............: avg=48.93µs min=0s med=0s max=82.25ms p(90)=0s p(95)=0s + http_req_sending...............: avg=20.41µs min=0s med=0s max=66.9ms p(90)=0s p(95)=0s + http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s + http_req_waiting...............: avg=1.22ms min=0s med=1.01ms max=91.78ms p(90)=2.05ms p(95)=2.52ms + http_reqs......................: 5767303 52429.944719/s + iteration_duration.............: avg=1.45ms min=0s med=1.09ms max=108.91ms p(90)=2.34ms p(95)=2.82ms + iterations.....................: 5767303 52429.944719/s + vus............................: 1 min=1 max=100 + vus_max........................: 100 min=100 max=100 + + +running (1m50.0s), 000/100 VUs, 5767303 complete and 0 interrupted iterations +default ✓ [======================================] 000/100 VUs 1m50s \ No newline at end of file diff --git a/src/ModEndpoints/DependencyInjectionExtensions.cs b/src/ModEndpoints/DependencyInjectionExtensions.cs index 990d00c..5af83a1 100644 --- a/src/ModEndpoints/DependencyInjectionExtensions.cs +++ b/src/ModEndpoints/DependencyInjectionExtensions.cs @@ -23,6 +23,10 @@ public static IServiceCollection AddModEndpointsFromAssembly( //WebResultEndpoint components services.TryAddKeyedSingleton( WebResultEndpointDefinitions.DefaultResultToResponseMapperName); + services.TryAddKeyedSingleton( + WebResultEndpointDefinitions.PreferredSuccessStatusCodeCacheNameForResult); + services.TryAddKeyedSingleton( + WebResultEndpointDefinitions.PreferredSuccessStatusCodeCacheNameForResultOfT); services.TryAddScoped(); services.TryAddSingleton(); diff --git a/src/ModEndpoints/[WebResultEndpoint]/DefaultResultToResponseMapper.cs b/src/ModEndpoints/[WebResultEndpoint]/DefaultResultToResponseMapper.cs index 5437b59..3f380fe 100644 --- a/src/ModEndpoints/[WebResultEndpoint]/DefaultResultToResponseMapper.cs +++ b/src/ModEndpoints/[WebResultEndpoint]/DefaultResultToResponseMapper.cs @@ -11,24 +11,6 @@ namespace ModEndpoints; /// public class DefaultResultToResponseMapper : IResultToResponseMapper { - private static readonly int[] _successStatusCodePriorityListForResult = - [ - StatusCodes.Status204NoContent, - StatusCodes.Status200OK, - StatusCodes.Status201Created, - StatusCodes.Status202Accepted, - StatusCodes.Status205ResetContent - ]; - - private static readonly int[] _successStatusCodePriorityListForResultOfT = - [ - StatusCodes.Status200OK, - StatusCodes.Status201Created, - StatusCodes.Status202Accepted, - StatusCodes.Status204NoContent, - StatusCodes.Status205ResetContent - ]; - public async ValueTask ToResponseAsync( Result result, HttpContext context, @@ -39,25 +21,13 @@ public async ValueTask ToResponseAsync( return result.ToErrorResponse(); } - var producesList = context - .GetEndpoint()? - .Metadata - .GetOrderedMetadata(); - - if (producesList is null || producesList.Count == 0) - { - return result.ToResponse(); - } - - var producesCode = _successStatusCodePriorityListForResult - .Join( - producesList, - outer => outer, - inner => inner.StatusCode, - (outer, inner) => outer) - .FirstOrDefault(); + var preferredSuccessStatusCodeCache = context.RequestServices + .GetRequiredKeyedService( + WebResultEndpointDefinitions.PreferredSuccessStatusCodeCacheNameForResult); + var preferredCode = preferredSuccessStatusCodeCache + .GetStatusCode(context); - switch (producesCode) + switch (preferredCode) { case StatusCodes.Status204NoContent: return result.ToResponse(); @@ -101,25 +71,13 @@ public async ValueTask ToResponseAsync( return result.ToErrorResponse(); } - var producesList = context - .GetEndpoint()? - .Metadata - .GetOrderedMetadata(); - - if (producesList is null || producesList.Count == 0) - { - return result.ToResponse(); - } - - var producesCode = _successStatusCodePriorityListForResultOfT - .Join( - producesList, - outer => outer, - inner => inner.StatusCode, - (outer, inner) => outer) - .FirstOrDefault(); + var preferredSuccessStatusCodeCache = context.RequestServices + .GetRequiredKeyedService( + WebResultEndpointDefinitions.PreferredSuccessStatusCodeCacheNameForResultOfT); + var preferredCode = preferredSuccessStatusCodeCache + .GetStatusCode(context); - switch (producesCode) + switch (preferredCode) { case StatusCodes.Status200OK: return result.ToResponse(); diff --git a/src/ModEndpoints/[WebResultEndpoint]/IPreferredSuccessStatusCodeCache.cs b/src/ModEndpoints/[WebResultEndpoint]/IPreferredSuccessStatusCodeCache.cs new file mode 100644 index 0000000..6d8fcdd --- /dev/null +++ b/src/ModEndpoints/[WebResultEndpoint]/IPreferredSuccessStatusCodeCache.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Http; + +namespace ModEndpoints; + +internal interface IPreferredSuccessStatusCodeCache +{ + int? GetStatusCode(HttpContext context); +} diff --git a/src/ModEndpoints/[WebResultEndpoint]/PreferredSuccessStatusCodeCacheForResult.cs b/src/ModEndpoints/[WebResultEndpoint]/PreferredSuccessStatusCodeCacheForResult.cs new file mode 100644 index 0000000..c032a6e --- /dev/null +++ b/src/ModEndpoints/[WebResultEndpoint]/PreferredSuccessStatusCodeCacheForResult.cs @@ -0,0 +1,54 @@ +using System.Collections.Concurrent; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Metadata; + +namespace ModEndpoints; + +internal class PreferredSuccessStatusCodeCacheForResult : IPreferredSuccessStatusCodeCache +{ + private readonly int?[] _successStatusCodePriorityList = + [ + StatusCodes.Status204NoContent, + StatusCodes.Status200OK, + StatusCodes.Status201Created, + StatusCodes.Status202Accepted, + StatusCodes.Status205ResetContent + ]; + + private readonly ConcurrentDictionary _cache = new(); + + public int? GetStatusCode( + HttpContext context) + { + var endpoint = context.GetEndpoint(); + if (endpoint is null) + { + return null; + } + var endpointString = endpoint.ToString(); + if (endpointString is null) + { + return null; + } + return _cache.GetOrAdd( + endpointString, + (_, endpoint) => + { + var producesList = endpoint + .Metadata + .GetOrderedMetadata(); + if (producesList is null || producesList.Count == 0) + { + return null; + } + return _successStatusCodePriorityList + .Join( + producesList, + outer => outer, + inner => inner.StatusCode, + (outer, inner) => outer) + .FirstOrDefault(); + }, + endpoint); + } +} diff --git a/src/ModEndpoints/[WebResultEndpoint]/PreferredSuccessStatusCodeCacheForResultOfT.cs b/src/ModEndpoints/[WebResultEndpoint]/PreferredSuccessStatusCodeCacheForResultOfT.cs new file mode 100644 index 0000000..8e055b2 --- /dev/null +++ b/src/ModEndpoints/[WebResultEndpoint]/PreferredSuccessStatusCodeCacheForResultOfT.cs @@ -0,0 +1,54 @@ +using System.Collections.Concurrent; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Metadata; + +namespace ModEndpoints; + +internal class PreferredSuccessStatusCodeCacheForResultOfT : IPreferredSuccessStatusCodeCache +{ + private readonly int?[] _successStatusCodePriorityList = + [ + StatusCodes.Status200OK, + StatusCodes.Status201Created, + StatusCodes.Status202Accepted, + StatusCodes.Status204NoContent, + StatusCodes.Status205ResetContent + ]; + + private readonly ConcurrentDictionary _cache = new(); + + public int? GetStatusCode( + HttpContext context) + { + var endpoint = context.GetEndpoint(); + if (endpoint is null) + { + return null; + } + var endpointString = endpoint.ToString(); + if (endpointString is null) + { + return null; + } + return _cache.GetOrAdd( + endpointString, + (_, endpoint) => + { + var producesList = endpoint + .Metadata + .GetOrderedMetadata(); + if (producesList is null || producesList.Count == 0) + { + return null; + } + return _successStatusCodePriorityList + .Join( + producesList, + outer => outer, + inner => inner.StatusCode, + (outer, inner) => outer) + .FirstOrDefault(); + }, + endpoint); + } +} diff --git a/src/ModEndpoints/[WebResultEndpoint]/WebResultEndpointDefinitions.cs b/src/ModEndpoints/[WebResultEndpoint]/WebResultEndpointDefinitions.cs index 82e14e7..ee43f39 100644 --- a/src/ModEndpoints/[WebResultEndpoint]/WebResultEndpointDefinitions.cs +++ b/src/ModEndpoints/[WebResultEndpoint]/WebResultEndpointDefinitions.cs @@ -4,4 +4,7 @@ public static class WebResultEndpointDefinitions public const string DefaultResultToResponseMapperName = "DefaultResultToResponseMapper"; public const string InvalidRouteMessage = "No route matches the supplied values."; public const string HttpContextIsInvalidMessage = "Http context resolved from dependency injection is invalid."; + + internal const string PreferredSuccessStatusCodeCacheNameForResult = "PreferredSuccessStatusCodeCacheForResult"; + internal const string PreferredSuccessStatusCodeCacheNameForResultOfT = "PreferredSuccessStatusCodeCacheForResultOfT"; }