| | | 1 | | using System.Text.Json.Serialization; |
| | | 2 | | using Microsoft.AspNetCore.Mvc; |
| | | 3 | | using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; |
| | | 4 | | |
| | | 5 | | namespace ArturRios.Heimdall.WebApi.Binding; |
| | | 6 | | |
| | | 7 | | /// <summary> |
| | | 8 | | /// Model-binding configuration shared by the running API and the OpenAPI document generator. |
| | | 9 | | /// |
| | | 10 | | /// Both call <see cref="Configure" /> for the same reason they both call |
| | | 11 | | /// <c>SwaggerConfiguration.Configure</c>: tools/ArturRios.Heimdall.OpenApiGen builds its own |
| | | 12 | | /// <c>AddControllers()</c> rather than using <c>Startup</c>, so configuration registered in only |
| | | 13 | | /// one of the two would let the published document disagree with the API it documents. |
| | | 14 | | /// </summary> |
| | | 15 | | public static class ModelBindingConfiguration |
| | | 16 | | { |
| | | 17 | | public static void Configure(MvcOptions options) |
| | | 18 | | { |
| | 899 | 19 | | options.ModelMetadataDetailsProviders.Add(new ServerPopulatedBindingMetadataProvider()); |
| | | 20 | | |
| | | 21 | | // With nullable reference types on, MVC infers [Required] on every non-nullable string or |
| | | 22 | | // collection property, so a JSON null would invalidate ModelState and MalformedRequestFilter |
| | | 23 | | // would answer the generic "The request is malformed." before the handler runs — hiding the |
| | | 24 | | // field's own message ("Secret is required.") and skipping the refused-write audit entry |
| | | 25 | | // (NFR-09). FluentValidation owns required-ness; the filter means only "cannot be bound". |
| | 899 | 26 | | options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true; |
| | 899 | 27 | | } |
| | | 28 | | } |
| | | 29 | | |
| | | 30 | | /// <summary> |
| | | 31 | | /// Makes a <c>[JsonIgnore]</c> property non-bindable, so a value arriving in the query string |
| | | 32 | | /// cannot reach it and ApiExplorer does not publish it as a parameter. |
| | | 33 | | /// |
| | | 34 | | /// The commands and queries the mediator takes as input double as the wire DTOs, so a property |
| | | 35 | | /// the controller assigns from the route or the authenticated caller looks, to the framework, |
| | | 36 | | /// exactly like one the client sends. <c>[JsonIgnore]</c> marks the difference: it already |
| | | 37 | | /// excludes the property from a <c>[FromBody]</c> payload and its schema, and this provider |
| | | 38 | | /// extends the same statement to the <c>[FromQuery]</c> path, where System.Text.Json attributes |
| | | 39 | | /// otherwise mean nothing. <c>IsBindingAllowed = false</c> is what <c>[BindNever]</c> sets; |
| | | 40 | | /// <c>[BindNever]</c> itself is unavailable to the Application-layer projects these DTOs live |
| | | 41 | | /// in, which have no reference to the ASP.NET Core shared framework. |
| | | 42 | | /// </summary> |
| | | 43 | | public class ServerPopulatedBindingMetadataProvider : IBindingMetadataProvider |
| | | 44 | | { |
| | | 45 | | public void CreateBindingMetadata(BindingMetadataProviderContext context) |
| | | 46 | | { |
| | | 47 | | if (context.Attributes.OfType<JsonIgnoreAttribute>().Any()) |
| | | 48 | | { |
| | | 49 | | context.BindingMetadata.IsBindingAllowed = false; |
| | | 50 | | } |
| | | 51 | | } |
| | | 52 | | } |