Serve data as CSV with a ASP.NET Core Web API and consume it in a React application using Axios

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

Website Powered by WordPress.com.

Up ↑