diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs index 8d072b1ec1..926647376a 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs @@ -1,3 +1,4 @@ +using JsonApiDotNetCoreExample.DocAnnotations; using Microsoft.AspNetCore.Mvc; namespace JsonApiDotNetCoreExample.Controllers; @@ -38,6 +39,8 @@ public async Task PostAsync([FromBody] string? name) [HttpPut] [EndpointDescription("Returns another greeting text.")] [ProducesResponseType(StatusCodes.Status200OK, "text/plain")] + [RequiresAdmin] + [ExpiresOn("2030-01-01")] public IActionResult Put([FromQuery] string? name) { string result = $"Hi, {name}"; diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/OperationsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/OperationsController.cs index a5cb2ef2e3..cbe860145a 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/OperationsController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/OperationsController.cs @@ -1,12 +1,23 @@ +using System.ComponentModel.DataAnnotations; using JsonApiDotNetCore.AtomicOperations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; +using JsonApiDotNetCoreExample.DocAnnotations; +using Microsoft.AspNetCore.Mvc; namespace JsonApiDotNetCoreExample.Controllers; public sealed class OperationsController( IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IOperationsProcessor processor, IJsonApiRequest request, ITargetedFields targetedFields, IAtomicOperationFilter operationFilter) - : JsonApiOperationsController(options, resourceGraph, loggerFactory, processor, request, targetedFields, operationFilter); + : JsonApiOperationsController(options, resourceGraph, loggerFactory, processor, request, targetedFields, operationFilter) +{ + [HttpPost] + [RequiresAdmin] + public override Task PostOperationsAsync([Required] IList operations, CancellationToken cancellationToken) + { + return base.PostOperationsAsync(operations, cancellationToken); + } +} diff --git a/src/Examples/JsonApiDotNetCoreExample/DocAnnotations/ExpiresOnAttribute.cs b/src/Examples/JsonApiDotNetCoreExample/DocAnnotations/ExpiresOnAttribute.cs new file mode 100644 index 0000000000..d02c163f4b --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreExample/DocAnnotations/ExpiresOnAttribute.cs @@ -0,0 +1,7 @@ +namespace JsonApiDotNetCoreExample.DocAnnotations; + +[AttributeUsage(AttributeTargets.Method)] +internal sealed class ExpiresOnAttribute(string dateValue) : Attribute +{ + public DateOnly Value { get; } = DateOnly.Parse(dateValue); +} diff --git a/src/Examples/JsonApiDotNetCoreExample/DocAnnotations/RequiresAdminAttribute.cs b/src/Examples/JsonApiDotNetCoreExample/DocAnnotations/RequiresAdminAttribute.cs new file mode 100644 index 0000000000..600d1ac66f --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreExample/DocAnnotations/RequiresAdminAttribute.cs @@ -0,0 +1,4 @@ +namespace JsonApiDotNetCoreExample.DocAnnotations; + +[AttributeUsage(AttributeTargets.Method)] +internal sealed class RequiresAdminAttribute : Attribute; diff --git a/src/Examples/JsonApiDotNetCoreExample/DynamicDocumentationOperationFilter.cs b/src/Examples/JsonApiDotNetCoreExample/DynamicDocumentationOperationFilter.cs new file mode 100644 index 0000000000..d9fafcd72f --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreExample/DynamicDocumentationOperationFilter.cs @@ -0,0 +1,38 @@ +using System.Reflection; +using JetBrains.Annotations; +using JsonApiDotNetCoreExample.DocAnnotations; +using Microsoft.OpenApi; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace JsonApiDotNetCoreExample; + +[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] +internal sealed class DynamicDocumentationOperationFilter : IOperationFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + if (context.MethodInfo.GetCustomAttribute() != null) + { + UpdateDescription(operation, "**CAUTION**: This endpoint requires admin permissions."); + } + + var expiresAtAttribute = context.MethodInfo.GetCustomAttribute(); + + if (expiresAtAttribute != null) + { + UpdateDescription(operation, $"**NOTE: This endpoint will no longer be available after {expiresAtAttribute.Value:yyyy-MM-dd}.**"); + } + } + + private static void UpdateDescription(OpenApiOperation operation, string text) + { + if (string.IsNullOrEmpty(operation.Description)) + { + operation.Description = text; + } + else + { + operation.Description += $"\n\n{text}"; + } + } +} diff --git a/src/Examples/JsonApiDotNetCoreExample/GeneratedSwagger/JsonApiDotNetCoreExample.json b/src/Examples/JsonApiDotNetCoreExample/GeneratedSwagger/JsonApiDotNetCoreExample.json index f32c5983c0..25140b7a3d 100644 --- a/src/Examples/JsonApiDotNetCoreExample/GeneratedSwagger/JsonApiDotNetCoreExample.json +++ b/src/Examples/JsonApiDotNetCoreExample/GeneratedSwagger/JsonApiDotNetCoreExample.json @@ -86,7 +86,7 @@ "tags": [ "nonJsonApi" ], - "description": "Returns another greeting text.", + "description": "Returns another greeting text.\n\n**CAUTION**: This endpoint requires admin permissions.\n\n**NOTE: This endpoint will no longer be available after 2030-01-01.**", "parameters": [ { "name": "name", @@ -153,6 +153,7 @@ "operations" ], "summary": "Performs multiple mutations in a linear and atomic manner.", + "description": "**CAUTION**: This endpoint requires admin permissions.", "operationId": "postOperations", "requestBody": { "description": "An array of mutation operations. For syntax, see the [Atomic Operations documentation](https://jsonapi.org/ext/atomic/).", diff --git a/src/Examples/JsonApiDotNetCoreExample/Program.cs b/src/Examples/JsonApiDotNetCoreExample/Program.cs index 56448b271e..f5318b621e 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Program.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Program.cs @@ -10,6 +10,7 @@ using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.DependencyInjection.Extensions; using Scalar.AspNetCore; +using Swashbuckle.AspNetCore.SwaggerGen; [assembly: ExcludeFromCodeCoverage] @@ -32,6 +33,8 @@ static WebApplication CreateWebApplication(string[] args) // Add services to the container. ConfigureServices(builder); + builder.Services.AddOptions().Configure(options => options.OperationFilter()); + WebApplication app = builder.Build(); // Configure the HTTP request pipeline.