-1

I am using the library CsvHelper to read some data in a text file on the same location as the solution. The text file has about 12 000 lines, which isn't that many. But it takes like over 10 minutes or so and makes the page/browser say "This page isn't responding".

When reading the same file without using Tasks, but directly in the Main method, it takes less than a second to get the same amount (12 000) of records. Am I using the Task in a wrong way?

//method within the caller class
private static async Task<IResult> GetEmployees(IEmployeeData data)
    {
        try
        {
            return Results.Ok(await data.GetEmployees());
        }
        catch (Exception ex)
        {
            return Results.Problem(ex.Message);
        }
    }
    
//method within class that implements ICsvDataAccess
public  Task<IEnumerable<T>> LoadData<T>(string path, bool hasHeaderRecord = false, string delimiter = ";")
    {
        return Task.Run(() => 
        {
            var config = new CsvConfiguration(CultureInfo.InvariantCulture)
            {
                HasHeaderRecord = hasHeaderRecord, Delimiter = delimiter
            };

            using (var reader = new StreamReader(path))
            using (var csv = new CsvReader(reader, config))
            {
                return csv.GetRecords<T>().ToList().AsEnumerable();
            }
        });
    }   

//within EmployeeData class which implements IEmployeeData interface
private readonly ICsvDataAccess _file;
public Task<IEnumerable<EmployeeModel>> GetEmployees() =>
        _file.LoadData<EmployeeModel>(path: "data.txt");
        
        

public interface IEmployeeData
{
    Task<IEnumerable<EmployeeModel>> GetEmployees();
}       
        
    
public interface ICsvDataAccess
{
    Task<IEnumerable<T>> LoadData<T>(string path, bool hasHeaderRecord = false, string delimiter = ";");
}

Thanks!

KWC
  • 109
  • 1
  • 8
  • Have you stepped over the instructions? Which line exactly is taking so long? – djv Dec 10 '21 at 22:26
  • Could you rename one of the two `GetEmployees` methods? Currently your code snippet is quite confusing, because of this name conflict. – Theodor Zoulias Dec 10 '21 at 22:33
  • As a side note, asynchronous methods [by convention](https://learn.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap#naming-parameters-and-return-types) are named with an `Async` suffix. `LoadDataAsync` and `GetEmployeesAsync` are correct. Also the `LoadData` method violates the guideline for [not exposing asynchronous wrappers for synchronous methods](https://devblogs.microsoft.com/pfxteam/should-i-expose-asynchronous-wrappers-for-synchronous-methods/). – Theodor Zoulias Dec 10 '21 at 22:38
  • Are you missing a lot of _async/await_ on all those Task returning methods? – Steve Dec 10 '21 at 22:39
  • Does this ever actually complete? It sounds like you've hit a [deadlock](https://stackoverflow.com/a/15022170/2554810) by queueing a Task onto an ASP.NET thread that isn't intended for them. If you're going to asynchronously load the CSV you shouldn't be doing it with the request thread since you presumably want to return a response to the user that then has a loading spinner or something to wait on the CSV to be ready, so I'd suggest checking how the .NET web framework you're using intends you to work with async code. – IllusiveBrian Dec 10 '21 at 22:56

1 Answers1

0

Running GetRecords inside Task.Run doesn't make this code asynchronous, it only uses a second thread to execute the blocking code what the first thread could also execute. If you run this code in ASP.NET Core, where each request is served by a separate thread, the code would only waste a thread.

To really read records asynchronously, use GetRecordsAsync() and return an IAsyncEnumerable :

public async IAsyncEnumerable<T> LoadDataAsync<T>(string path, bool hasHeaderRecord = false, string delimiter = ";")
{

        var config = new CsvConfiguration(CultureInfo.InvariantCulture)
        {
            HasHeaderRecord = hasHeaderRecord, Delimiter = delimiter
        };

        using (var reader = new StreamReader(path))
        using (var csv = new CsvReader(reader, config))
        {
            await foreach(var rec in csv.GetRecords<T>())
            {
                yield return rec;
            }
        }
} 

The await foreach/yield return loop ensures the readers won't be disposed until client code that iterates over the IAsyncEnumerable finishes.

ASP.NET Core 5 and later support IAsyncEnumerable which means you can write :

[HttpGet]
public IAsyncEnumerable<ThatData> Get()
{
    return LoadDataAsync<ThatData>(...);
} 

or

[HttpGet]
public IActionResult Get()
{
    return Ok(LoadDataAsync<ThatData>(...);
}

ASP.NET Core 5 will buffer the results before serializing them to JSON, but ASP.NET Core 6 will start serializing the records without buffering

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236