Vaughan Reid's blog

Creating a CSV text to model custom model binder in ASP.NET Core

ASP.NET Core has quite a large selection of model binders built in that are able to convert http requests to simple and complex models in your actions. If there isn't one that matches your needs, you can always create a custom one. To show how, I'm going to create a custom model binder that converts csv formatted text to an IEnumerable of models using the great CsvHelper library.

Lets start with an endpoint that a client can post Contact data to. Eg:

 [Route("api/[controller]")]
 [ApiController]
 public class ContactController : ControllerBase
 {

	[HttpPost]
	public IActionResult Post(IEnumerable<Contact> records)
	{
		//Save records
		return Ok();
	}
}

public class Contact
{
	public string Name { get; set; }
	public string Email { get; set; }
}
	

The data is stored in a csv file and so it would be cleaner if they could just post the csv text and the Post action receives the IEnumerable of Contacts.

To do this I will create an IModelBinderProvider which will select my new Model binder if the argument type matches what I register in Startup.cs.


public class CsvFileToModelBinderProvider<T> : IModelBinderProvider
{
	public IModelBinder GetBinder(ModelBinderProviderContext context)
	{
		if (context.Metadata.ModelType.IsAssignableFrom(typeof(IEnumerable<T>)))
		{
			return new CsvFileToModelBinder<T>();
		}
	return null;
	}
}

This is my simple model binder using the CsvHelper package.


public class CsvFileToModelBinder<T> : IModelBinder
{
	public async Task BindModelAsync(ModelBindingContext bindingContext)
	{
		if (bindingContext == null)
		{
			throw new ArgumentNullException(nameof(bindingContext));
		}

		using var streamReader = new StreamReader(bindingContext.HttpContext.Request.Body);
		string text = await streamReader.ReadToEndAsync();

		using var stringReader = new StringReader(text);
		using var csv = new CsvReader(stringReader, CultureInfo.InvariantCulture);

		var records = csv.GetRecords<T>().ToArray();

		bindingContext.Result = ModelBindingResult.Success(records);

		return;
	}
}

I created an extension method to Register it for a type and registered it in the ConfigureServices method in Startup.cs. You will notice that I added the Model Binder as the first element in the list of providers and the reason for this is that the engine will stop searching for a provider if it finds one that passes its own validation.


public static class ModelBinderExtensions
{
	public static void AddCsvFileToModelBinder<T>(this MvcOptions options)
	{
		options.ModelBinderProviders.Insert(0, new CsvFileToModelBinderProvider<T>());
	}
}

public void ConfigureServices(IServiceCollection services)
{
	services.AddControllers(options =>
	{
		options.AddCsvFileToModelBinder<Contact>();
	});

	//Removed for brevity
}

To show that this is now working I posted csv text in Postman to the endpoint:

ModelBindingRestCall

and the model was populated in the action.

ModelBindingResult