Created
February 4, 2025 12:53
-
-
Save MosheL/bd9edf2b2db0f7eb0ae554e482f28e7c to your computer and use it in GitHub Desktop.
FormOrJsonModelBinder: allow both JSON and Form (multipart/form-data or url-encoded) POST to a ASP.net core MVC Action.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace core.Tools { | |
using System.Threading.Tasks; | |
using Microsoft.AspNetCore.Http; | |
using Microsoft.AspNetCore.Mvc; | |
using Microsoft.AspNetCore.Mvc.Infrastructure; | |
using Microsoft.AspNetCore.Mvc.ModelBinding; | |
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; | |
using Microsoft.Extensions.Logging; | |
using Microsoft.Extensions.Options; | |
public class FormOrJsonAttribute : ModelBinderAttribute { | |
public FormOrJsonAttribute() : base(typeof(FormOrJsonModelBinder)) { | |
// Optionally, you can set the BindingSource if needed. | |
// BindingSource = BindingSource.Custom; | |
} | |
} | |
public class FormOrJsonModelBinder : IModelBinder { | |
public async Task BindModelAsync(ModelBindingContext bindingContext) { | |
var request = bindingContext.HttpContext.Request; | |
// If the request contains form data, delegate to the default binder. | |
if (request.HasFormContentType) { | |
var metadataProvider = (IModelMetadataProvider)bindingContext.HttpContext.RequestServices | |
.GetService(typeof(IModelMetadataProvider)); | |
var cleanMetadata = metadataProvider.GetMetadataForType(bindingContext.ModelType); | |
// Create a new BindingInfo. | |
// IMPORTANT: Set the BindingSource to Form, so that the default binder will look in the form data. | |
var bindingInfo = new BindingInfo { BindingSource = BindingSource.Form }; | |
// Create a new ModelBinderFactoryContext using the clean metadata. | |
var factoryContext = new ModelBinderFactoryContext { | |
BindingInfo = bindingInfo, | |
Metadata = cleanMetadata, | |
CacheToken = bindingContext.ModelType, | |
}; | |
// Get the binder factory and create the default binder. | |
var binderFactory = (IModelBinderFactory)bindingContext.HttpContext.RequestServices | |
.GetService(typeof(IModelBinderFactory)); | |
var defaultBinder = binderFactory.CreateBinder(factoryContext); | |
// Delegate binding to the default binder. | |
await defaultBinder.BindModelAsync(bindingContext); | |
} | |
// If the Content-Type indicates JSON, delegate to the BodyModelBinder. | |
else if (request.ContentType?.StartsWith("application/json") == true) { | |
// Resolve required services. | |
var readerFactory = (IHttpRequestStreamReaderFactory)bindingContext.HttpContext.RequestServices | |
.GetService(typeof(IHttpRequestStreamReaderFactory))!; | |
var optionsAccessor = (IOptions<MvcOptions>)bindingContext.HttpContext.RequestServices | |
.GetService(typeof(IOptions<MvcOptions>))!; | |
var mvcOptions = optionsAccessor.Value; | |
var loggerFactory = (ILoggerFactory)bindingContext.HttpContext.RequestServices | |
.GetService(typeof(ILoggerFactory))!; | |
// Note: Order of parameters: inputFormatters, readerFactory, mvcOptions, loggerFactory. | |
var bodyBinder = new BodyModelBinder(mvcOptions.InputFormatters, readerFactory, loggerFactory); | |
await bodyBinder.BindModelAsync(bindingContext); | |
} | |
else { | |
// Unsupported Content-Type; fail the binding. | |
bindingContext.Result = ModelBindingResult.Failed(); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment