Skip to content

Instantly share code, notes, and snippets.

@abdusco
Created March 30, 2021 18:32
Show Gist options
  • Save abdusco/919d2dd257bacc1e61cc9ff492fb2a2f to your computer and use it in GitHub Desktop.
Save abdusco/919d2dd257bacc1e61cc9ff492fb2a2f to your computer and use it in GitHub Desktop.
ASP.NET Core json query string with Swagger support
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Net.Http;
using System.Net.Mime;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace MvcPlayground
{
public class FromJsonQueryAttribute : ModelBinderAttribute
{
public FromJsonQueryAttribute()
{
BinderType = typeof(JsonQueryBinder);
BindingSource = BindingSource.Custom;
}
public FromJsonQueryAttribute(string queryParameterName) : this()
{
Name = queryParameterName;
}
}
class JsonQueryBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.FieldName).FirstValue;
if (value == null)
{
return Task.CompletedTask;
}
var logger = bindingContext.HttpContext.RequestServices.GetRequiredService<ILogger<JsonQueryBinder>>();
try
{
var parsed = JsonSerializer.Deserialize(value, bindingContext.ModelType, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
});
bindingContext.Result = ModelBindingResult.Success(parsed);
}
catch (Exception e)
{
logger.LogError(e, "Failed to bind parameter");
bindingContext.Result = ModelBindingResult.Failed();
}
return Task.CompletedTask;
}
}
internal static class SwaggerGenExtensions
{
public static SwaggerGenOptions AddJsonQuerySupport(this SwaggerGenOptions options)
{
options.OperationFilter<JsonQueryOperationFilter>();
return options;
}
private class JsonQueryOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (context.ApiDescription.HttpMethod != HttpMethod.Get.Method || operation.Parameters == null)
{
return;
}
var jsonQueryParams = context.ApiDescription.ActionDescriptor.Parameters
.Cast<ControllerParameterDescriptor>()
.Where(ad => ad.BindingInfo.BinderType == typeof(JsonQueryBinder))
.ToList();
if (!jsonQueryParams.Any())
{
return;
}
operation.Parameters = operation.Parameters.ToList()
.Select(p =>
{
var actionParam = jsonQueryParams.FirstOrDefault(qp => qp.Name == p.Name);
if (actionParam == null)
{
return p;
}
var schema = context.SchemaGenerator.GenerateSchema(actionParam.ParameterType, context.SchemaRepository);
return new OpenApiParameter
{
Name = p.Name,
In = ParameterLocation.Query,
Content = new Dictionary<string, OpenApiMediaType>()
{
[MediaTypeNames.Application.Json] = new OpenApiMediaType()
{
Schema = schema
}
}
};
}).ToList();
// throw new NotImplementedException();
}
}
}
[ApiController]
[Route("api/[controller]")]
public class BindingController : ControllerBase
{
[HttpGet("test/{id}")]
public Task<ActionResult> PerformQuery(string id, [FromJsonQuery] MyQuery query)
{
return Task.FromResult<ActionResult>(Ok(query));
}
}
public record MyQuery(MyFilter Filter, [Required] string Sort);
public record MyFilter(string Query);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment