How to receive a request with CSV data in ASP.NET Core
By FoxLearn 12/26/2024 6:06:04 AM 23
- Receive CSV as a file in a
multipart/form-data
request. - Receive CSV as a string in a
text/csv
request.
In this guide, we'll walk through examples for both methods, using the CsvHelper library to parse CSV data into model objects and perform model validation.
1. Receiving a CSV File
The client can upload a CSV file in a multipart/form-data
request, which you can access via Request.Form.Files
or by using IFormFile
parameters.
using CsvHelper; using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; public class Book { public string Title { get; set; } public string Author { get; set; } public int PublishedYear { get; set; } } [ApiController] [Route("api/[controller]")] public class BooksController : ControllerBase { [HttpPost("upload")] public async Task<IActionResult> UploadBooksCsv() { var books = new List<Book>(); IFormFile csvFile = Request.Form.Files.FirstOrDefault(); if (csvFile == null) { return BadRequest("No CSV file uploaded."); } try { using (var reader = new StreamReader(csvFile.OpenReadStream())) { using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); // Process each book record in the CSV await foreach (var book in csv.GetRecordsAsync<Book>(HttpContext.RequestAborted)) { // Perform validation on each book record as it's read if (!TryValidateModel(book)) { // Return the first validation error return ValidationProblem(); } books.Add(book); } } // Optional: Validate the entire list after parsing (if needed for aggregate errors) if (!ModelState.IsValid) { return ValidationProblem(ModelState); } // Process the list of books (e.g., save to database) return Ok($"Successfully uploaded {books.Count} books."); } catch (Exception ex) { // Handle CSV parsing errors or file reading exceptions return StatusCode(500, $"Error processing CSV file: {ex.Message}"); } } }
To test this, use Postman to send the CSV file in a POST
request:
2. Receiving a CSV String (text/csv)
Another method is to send CSV data as a string in the request body with the text/csv
content type. This allows you to directly read the CSV data or use a custom InputFormatter
.
using CsvHelper; using System.Globalization; using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using System.IO; public class Book { public string Title { get; set; } public string Author { get; set; } public int YearPublished { get; set; } } [ApiController] [Route("api/[controller]")] public class BooksController : ControllerBase { [HttpPost] [Consumes("text/csv")] // Ensures the request content type is text/csv public async Task<IActionResult> PostBooksCsv() { var books = new List<Book>(); try { using (var reader = new StreamReader(Request.Body)) { using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); // Process each book record in the CSV await foreach (var book in csv.GetRecordsAsync<Book>(HttpContext.RequestAborted)) { // Perform validation on each book record as it's read if (!TryValidateModel(book)) { // Return the first validation error encountered return ValidationProblem(); } books.Add(book); } } // Return the result once all records are processed return Ok($"Successfully posted {books.Count} books."); } catch (Exception ex) { // Handle CSV parsing errors or file reading exceptions return StatusCode(500, $"Error processing CSV file: {ex.Message}"); } } }
3. InputFormatter to Handle text/csv with CsvHelper
An InputFormatter
allows you to abstract CSV parsing from your controllers. This custom formatter handles text/csv
requests by parsing the CSV data and deserializing it into model objects.
using CsvHelper; using System.Globalization; using Microsoft.AspNetCore.Mvc.Formatters; using System.Collections.Generic; public class CsvStringInputFormatter : InputFormatter { public CsvStringInputFormatter() { SupportedMediaTypes.Add("text/csv"); } public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context) { try { // Ensure we can handle the expected list type var listType = context.ModelType.GenericTypeArguments[0]; var list = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(listType)); // Reading the body stream and parsing the CSV data using var reader = new StreamReader(context.HttpContext.Request.Body); using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); // Read and add each record to the list await foreach (var record in csv.GetRecordsAsync(listType)) { list.Add(record); } // Return the successfully parsed list return InputFormatterResult.Success(list); } catch (CsvHelperException ex) { // Specific exception handling for CsvHelper context.ModelState.TryAddModelError("Csv", $"CSV Parsing Error: {ex.Message}"); return InputFormatterResult.Failure(); } catch (Exception ex) { // General exception handling context.ModelState.TryAddModelError("Csv", $"An unexpected error occurred: {ex.Message}"); return InputFormatterResult.Failure(); } } protected override bool CanReadType(Type type) { // Ensures only lists of a generic type (List<T>) are accepted return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>); } }
To register the CsvStringInputFormatter
in your application, make sure you add it to the list of input formatters in ConfigureServices
:
public void ConfigureServices(IServiceCollection services) { services.AddControllers(options => { options.InputFormatters.Add(new CsvStringInputFormatter()); }); }
This will ensure that the formatter is used when the Content-Type
is text/csv
.
Now, in your action method, simply define a List<T>
parameter, and model validation will be handled automatically:
[HttpPost] [Consumes("text/csv")] public IActionResult Post(List<Book> books) { // Process the book records... return Ok($"Posted {books.Count} book(s)"); }
The [Consumes("text/csv")]
attribute ensures that the API only accepts requests with the text/csv
content type. If you remove it, the API would accept other formats as well.
4. Extension Method: CsvToListAsync()
To reduce redundancy in CSV parsing, you can create an extension method that works with both file and string-based inputs.
using CsvHelper; using System.Globalization; using System.IO; public static class RequestExtensions { public static async Task<List<T>> CsvToListAsync<T>(this Stream stream) { var list = new List<T>(); try { using var reader = new StreamReader(stream); using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); // Process records asynchronously, adding each to the list await foreach (var record in csv.GetRecordsAsync<T>()) { list.Add(record); } } catch (Exception ex) { // Handle any potential exceptions (e.g., file read errors, CSV parsing errors) // Log the error or rethrow as needed. throw new InvalidOperationException("Error processing CSV data", ex); } return list; } }
To use this method with the request body or file:
[HttpPost] [Consumes("text/csv")] public async Task<IActionResult> Post() { var books = await Request.Body.CsvToListAsync<Book>(); if (!TryValidateModel(movies)) return ValidationProblem(); return Ok($"Posted {books.Count} book(s)"); }
For file input:
IFormFile csvFile = Request.Form.Files.First(); var books = await csvFile.OpenReadStream().CsvToListAsync<Book>();
5. Triggering Model Validation Manually
When reading the request body directly, model validation isn't triggered automatically. You need to manually invoke TryValidateModel()
to validate your models:
if (!TryValidateModel(book)) return ValidationProblem();
When validation fails, it returns a 400 response with the validation errors:
{ "type": "https://foxlearn.com/posts/search", "title": "Validation failed for one or more fields.", "status": 400, "traceId": "00-45e16d3a4f1c1234bc27ab123dabc18b-ef4bc45f798-00", "errors": { "Email": [ "The Email field is not a valid email address." ], "Age": [ "Age must be a number between 18 and 100." ] } }
You can validate either individual models as you read them or perform validation on the entire list after the loop.
This article covers two methods for handling CSV data in a web API: receiving it as a file or a string. Using CsvHelper allows for easy parsing and validation of the data, while InputFormatters offer a cleaner way to handle CSV requests.
- How to get Url Referrer in ASP.NET Core
- How to get HttpContext.Current in ASP.NET Core
- How to read Configuration Values from appsettings.json in ASP.NET Core
- How to add link parameter to asp tag helpers in ASP.NET Core
- How to set json serializer settings in ASP.NET Core
- Error 0x80004005 when starting ASP.NET .NET Core 2.0 in IIS
- How to use CORS in ASP.NET Core
- How to Send Emails in ASP.NET Core