Recently I implemented a CSV export feature in an application that consists of a ASP.NET Core Web API backend and a React frontend. For creating/writing the CSV file, I used the CsvHelper library – a .NET library for reading and writing CSV files.
Version overview
- .NET Core 7
- React
^18.2.0 - Axios
^1.3.6
Even though this is a fairly straightforward functionality, there are a few details to consider to make it all work.
Let’s have a look at the concrete implementation.
ASP.NET Core Web API / Backend
ArbitraryModel.cs
The model class that represents a data row in the resulting CSV export.
namespace ArbitrarySolution.Shared.Models;
public class ArbitraryModel
{
public string Email { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
}
ArbitraryController.cs
The REST API endpoint that serves the CSV export.
using System.Net.Mime;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ArbitrarySolution.Services;
using ArbitrarySolution.Shared.Models;
namespace ArbitrarySolution.Server.Controllers;
[Authorize(Policy = AuthorizationPolicies.AssignmentToArbitraryRoleRequired)]
[ApiController]
[Route("api/[controller]")]
public class ArbitraryController : ControllerBase
{
private readonly ArbitraryService _arbitraryService;
private readonly ExportService _exportService;
public ArbitraryController(ArbitraryService arbitraryService, ExportService exportService)
{
_arbitraryService = arbitraryService;
_exportService = exportService;
}
[HttpGet("export")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> ExportCsv()
{
var dataToBeExported = await _arbitraryService.GetDataToBeExportedAsync();
var fileContent = await _exportService.GetCsvAsync(dataToBeExported);
return File(fileContent, MediaTypeNames.Application.Octet);
}
}
ExportService.cs
using System.Globalization;
using CsvHelper;
using CsvHelper.Configuration;
using Microsoft.Extensions.Logging;
using ArbitrarySolution.Shared.Models;
namespace ArbitrarySolution.Services;
public class ExportService
{
private readonly ILogger<ExportService> _logger;
public ExportService(ILogger<ExportService> logger)
{
_logger = logger;
}
public async Task<byte[]> GetCsvAsync(IEnumerable<ArbitraryModel> arbitraryModels)
{
_logger.LogInformation("Generating CSV ...");
await using var memoryStream = new MemoryStream();
await using var streamWriter = new StreamWriter(memoryStream);
await using var csvWriter = new CsvWriter(streamWriter, CultureInfo.InvariantCulture);
// register mapping configuration for ArbitraryModel class
csvWriter.Context.RegisterClassMap<ArbitraryModelMap>();
await csvWriter.WriteRecordsAsync(arbitraryModels);
// flush stream writer as recommended by the docs
await streamWriter.FlushAsync();
_logger.LogInformation("Generating CSV SUCCEEDED");
return memoryStream.ToArray();
}
public class ArbitraryModelMap : ClassMap<ArbitraryModel>
{
public ArbitraryModelMap()
{
Map(m => m.Email).Index(0).Name("Email");
Map(m => m.Name).Index(1).Name("Name");
}
}
}
More details concerning writing/creating CSV files can be found in the official docs.
React / Frontend
HttpClient.ts
The HTTP client that uses axios for doing requests against the backend. To make the CSV download work it’s important to set the response type correctly.
import axios, { AxiosInstance } from "axios";
export class HttpClient {
private readonly axiosInstance: AxiosInstance;
constructor() {
this.axiosInstance = axios.create();
}
async export<TResponse, TParameters>(
url: string,
params: TParameters
): Promise<TResponse> {
const response = await this.axiosInstance.get<TResponse>(
`${HttpClient.getApiUrl() + url}`,
{ params, responseType: "blob" }
);
return response.data;
}
public static getApiUrl() {
const backendHost = HttpClient.getCurrentHost();
return `${backendHost}/api/`;
}
public static getCurrentHost() {
const host = window.location.host;
const url = `${window.location.protocol}//${host}`;
return url;
}
}
ExportService.ts
import { createAndClickLink } from "../components/common/hooks/useDownloadFile";
import { ArbitraryModel } from "../models/ArbitraryModel";
import { QueryConfig } from "../models/common/ServiceCalls/QueryConfig";
import { HttpClient } from "./HttpClient";
export class ExportService {
public baseQueryKey = ["Arbitrary"];
constructor(protected httpClient: HttpClient) {}
public exportArbitrary = async () =>
(async () => {
const response = await this.httpClient.export<
Blob,
{}
>("arbitrary/export", { });
createAndClickLink({
content: response,
fileName: `export.csv`,
});
return response;
})();
public exportArbitraryQueryConfig = (): QueryConfig<Blob> => ({
queryFn: () => this.exportArbitrary(),
queryKey: [
...this.baseQueryKey,
"export",
],
});
}
useDownloadFile.ts
React hook that initiates the download of the CSV file.
interface CreateAndClickLinkParams {
content: Blob;
fileName: string;
}
export const createAndClickLink = ({
content,
fileName,
}: CreateAndClickLinkParams): void => {
const blobUrl = window.URL.createObjectURL(content);
const link = document.createElement("a");
link.href = blobUrl;
link.download = fileName;
document.body.appendChild(link);
link.click();
window.URL.revokeObjectURL(blobUrl);
};
interface UseDownloadAsFileReturn {
downloadAsFile: (params: CreateAndClickLinkParams) => void;
}
export const useDownloadAsFile = (): UseDownloadAsFileReturn => ({
downloadAsFile: (params: CreateAndClickLinkParams) => {
createAndClickLink(params);
},
});
That’s it. I hope it helps some people out there.

Leave a comment