Reputation: 19
I am creating a simple project with Blazor in C# where users can upload a CSV file and the app processes this file asynchronously to an object so that it can create a database record of this object.
I am getting stuck at processing the CSV file. I tried to process the CSV file myself first and this worked but read on several posts here that it would be wiser to use a library like CSVHelper. I am trying to use this library but it does not seem like it is doing anything and I cannot figure out what is going wrong.
I am using console.Writeline to check if anything is in the records collection but it seems empty. Can anybody explain what I am doing wrong or what I am missing?
This is the page:
@using CsvHelper
@using System.Globalization
@using System.Text
@using CsvHelper.Configuration
@using CompanyName.Data
@using CompanyName.Services.CSV
@page "/uploadcsv"
<PageTitle>Upload CSV file</PageTitle>
<h1>Upload CSV file</h1>
<p>
<label>
Upload your CSV here:
<InputFile OnChange="@LoadFile" multiple />
</label>
</p>
@code {
private async Task LoadFile(InputFileChangeEventArgs e)
{
foreach (var file in e.GetMultipleFiles())
{
var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{
Delimiter = ";",
Encoding = Encoding.UTF8
};
using (var reader = new StreamReader(file.OpenReadStream()))
using (var csv = new CsvReader(reader, config))
{
csv.Context.RegisterClassMap<OrderMap>();
var records = csv.GetRecordsAsync<Order>();
await foreach (var order in records)
{
Console.WriteLine(order.OrderNumber);
}
}
}
}
}
This is the class that I use:
namespace CompanyName.Data;
public class Order
{
public string OrderNumber { get; set; }
public string RecipientName { get; set; }
public string RecipientStreet { get; set; }
public string RecipientPostalCode { get; set; }
public string RecipientCity { get; set; }
public string CustomerNotes { get; set; }
public string TypeOfCargo { get; set; }
public int ShipmentQuantity { get; set; }
public string? LocationId { get; set; }
}
This is the classmap to map the CSV to the object:
using CsvHelper.Configuration;
using CompanyName.Data;
namespace CompanyName.Services.CSV;
public sealed class OrderMap : ClassMap<Order>
{
public OrderMap()
{
Map(o => o.OrderNumber).Name("ActivityId");
Map(o => o.RecipientName).Name("LocationName");
Map(o => o.RecipientStreet).Name("LocationStreet");
Map(o => o.RecipientPostalCode).Name("LocationPostalCode");
Map(o => o.RecipientCity).Name("LocationCity");
Map(o => o.CustomerNotes).Name("ActivityNotes");
Map(o => o.TypeOfCargo).Name("ShipmentTypeOfCargo");
Map(o => o.ShipmentQuantity).Name("ShipmentQuantity");
Map(o => o.LocationId).Ignore();
}
}
I have tried altering it to a synchronous flow (private void LoadFile with
csv.GetRecords<Order>()
and without using await) and this results in the same exception that I found when debugging the async task only now it immediately shows this when uploading the CSV. This is the stacktrace:
System.NotSupportedException: Synchronous reads are not supported.
at Microsoft.AspNetCore.Components.Forms.BrowserFileStream.Read(Byte[] buffer, Int32 offset, Int32 count)
at System.IO.StreamReader.ReadBuffer(Span`1 userBuffer, Boolean& readToUserBuffer)
at System.IO.StreamReader.ReadSpan(Span`1 buffer)
at System.IO.StreamReader.Read(Char[] buffer, Int32 index, Int32 count)
at CsvHelper.CsvParser.Read()
at CsvHelper.CsvReader.Read()
at CsvHelper.CsvReader.GetRecords[T]()+MoveNext()
at CompanyNameLogistics.Pages.UploadCSV.LoadFile(InputFileChangeEventArgs e) in C:\Users\User\Documents\CompanyNameLogistics\CompanyNameLogistics\Pages\UploadCSV.razor:line 33`
Upvotes: 1
Views: 5976
Reputation: 29548
I think you have encountered a bug in the CsvHelper library. "dbc" pointed to issue #1751 in the CsvHelper repo, and I believe you are experiencing the same issue, which is a deadlock caused by async code that doesn't call ConfigureAwait(false)
. You can read about the details of that [here]:(https://devblogs.microsoft.com/dotnet/configureawait-faq/)
This issue happens when the code runs under a SynchronizationContext, which it appears is the case for Blazor server, as well as in windows GUI applications.
I opened a pull request which should fix this issue, though you would also need to ConfigureAwait(false)
all of your async invocations:
https://github.com/JoshClose/CsvHelper/pull/2007
In the mean time, you can pull and build the "configureawait" branch at: https://github.com/MarkPflug/CsvHelper/
That should at least allow you to answer if this will fix your issue.
Update
The proposed pull was committed as of CsvHelper version 29.0.0:
Missing
ConfigureAwait(false)
added to async calls.
Upvotes: 3
Reputation: 19
I have worked around my issue by copying the browserfile and writing it into a project folder and then opening this local file into a new filestream and feeding that into a readstream which is then processed by CSVHelper.
Not sure if this is the right approach and it probably goes against the advice of Microsoft of taking the entire file into memory but since I only use very small files in this use case it should not be much of a problem.
Since I am a beginner please respond if I can make this more efficiënt or if there is a better way to do this. Happy to take some feedback! Otherwise thanks for all the help!
This is the code I used. I did use a new (smaller) class (OrderTest) with less properties and a new mapper (CsvOrderTestMapping) just for figuring this out so it does not entirely match with my code in the original post.
@code {
private async Task LoadFile(InputFileChangeEventArgs e)
{
foreach (var file in e.GetMultipleFiles())
{
var path = Path.Combine(Environment.CurrentDirectory, @"TempFiles\", file.Name);
Console.WriteLine(path);
await using (FileStream fsWrite = new(path, FileMode.Create))
{
await file.OpenReadStream().CopyToAsync(fsWrite);
}
var configuration = new CsvConfiguration(CultureInfo.InvariantCulture)
{
Encoding = Encoding.UTF8,
Delimiter = ";",
};
await using(var fsRead = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (var reader = new StreamReader(fsRead, Encoding.UTF8))
using (var csv = new CsvReader(reader, configuration))
{
csv.Context.RegisterClassMap<CsvOrderTestMapping>();
var data = csv.GetRecordsAsync<OrderTest>();
await foreach (var order in data)
{
Console.WriteLine(order.RecipientName+ ", " + order.RecipientStreet+ ", " + order.OrderNumber);
}
}
}
File.Delete(path);
}
}
}
Upvotes: 0
Reputation: 7194
You are calling an async method, but not awaiting the results. Change this:
var records = csv.GetRecordsAsync<Order>();
To this:
var records = await csv.GetRecordsAsync<Order>();
Upvotes: 0