Skip to content

Instantly share code, notes, and snippets.

@MosheL
Created February 4, 2025 12:53
Show Gist options
  • Save MosheL/bd9edf2b2db0f7eb0ae554e482f28e7c to your computer and use it in GitHub Desktop.
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.
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