To read a CSV file in ASP.NET, accept the upload, parse it with a real CSV
parser, validate each record, and show the rows in a table. In classic
ASP.NET Web Forms you do that with
FileUpload,
TextFieldParser, and a
GridView. In ASP.NET Core you do it with
IFormFile, CsvHelper, and a Razor Page.
This article covers both. Each part builds a complete working page for a small Codingvila order import. Part 1 (ASP.NET Core) uses C#, and Part 2 (Web Forms) shows every piece of code in both C# and VB.NET. You don't need ASP.NET experience, but you should be comfortable reading basic C# or VB.NET.
In this article:
- Part 1: Upload and read a CSV file in ASP.NET Core (.NET 10)
- Part 2: Read a CSV file in ASP.NET Web Forms and show it in a GridView (.NET Framework)
- Which CSV parsing approach should you use?
- Frequently asked questions
Which part do you need? Use Part 1 for a new project or any app on current .NET. Use Part 2 if you maintain an existing Web Forms site on the .NET Framework. Web Forms does not exist in ASP.NET Core, so the two parts use different controls and code.
What is a CSV file?
CSV stands for comma-separated values. It's a plain-text file where each line is one record, and the fields in that record are separated by commas. Spreadsheet programs such as Excel can open and save CSV files, which makes them a common way to move tabular data between systems.
One detail trips up many beginners. A field that contains a comma must be
wrapped in double quotes, for example
"Mehta, Rahul". A parser that simply splits
each line on commas treats that value as two fields and breaks the row. The
same quoting rule covers line breaks inside a field, and a literal double
quote inside a quoted field is written as two double quotes ("").
In this tutorial each line has exactly six values:
Id,
CustomerName,
Item, Qty,
Price, and
Amount. Save the following text as
codingvila-orders.csv so you can test both
parts. The fourth row contains a quoted comma on purpose.
1,Maria Gonzalez,Notebook,10,2.50,25.00
2,James Carter,USB cable,3,7.99,23.97
3,Aisha Patel,Desk lamp,1,24.00,24.00
4,"Mehta, Rahul",Stapler,2,5.25,10.50
If your file starts with a header line, both pages let you skip it with a
checkbox. The header line for this file would be
Id,CustomerName,Item,Qty,Price,Amount.
Part 1: Upload and read a CSV file in ASP.NET Core (.NET 10)
This part builds a small order import page with ASP.NET Core Razor Pages. Part 2 builds the same page in Web Forms. As of October 2026, .NET 10 is the current long-term support (LTS) release, and Microsoft says it is supported through November 2028. .NET 8 and .NET 9 both reach end of support on November 10, 2026, and .NET 11 is expected in November 2026. The code below uses long-standing APIs, but retest it if you move to a newer version.
Before you start
-
The .NET 10 SDK. Run
dotnet --versionin a terminal to check what you have. -
Any editor: Visual Studio, Visual Studio Code, or a plain text editor plus
the
dotnetcommand line. - Basic C# knowledge.
-
The sample file
codingvila-orders.csvfrom the start of this article.
The code relies on two defaults of the
webapp template: implicit
using statements (so
List,
Task,
Stream, and
Path need no
using line) and nullable reference types
(the ? after a type name). If you turned
either off in your .csproj file, add the
missing using lines yourself.
How Web Forms concepts map to ASP.NET Core
If you know Web Forms, this table shows where each piece goes in ASP.NET Core.
| Web Forms | ASP.NET Core Razor Pages |
|---|---|
FileUpload control |
IFormFile property bound to an
<input type="file">
|
FileUpload1.HasFile |
Check that the IFormFile is not
null and its
Length is greater than zero
|
| Code-behind button click method |
A page handler such as OnPostAsync
|
TextFieldParser |
CsvHelper (a NuGet package) |
DataTable and
GridView
|
A list of typed objects and an HTML
<table> in a
.cshtml file
|
| View state | No view state. Data you show must be produced by the handler on each request. |
maxRequestLength in
Web.config
|
Kestrel and filter limits such as
[RequestSizeLimit]
|
How the ASP.NET Core import works
The sequence diagram below shows one upload. The browser posts the form to
the page model (IndexModel). The page model
checks the file name and size, then passes the upload stream to the importer
class (OrderCsvImporter), which returns an
ImportResult holding either the parsed rows
or an error message. The page then renders as HTML, with a table of rows or
the error message.
Step 1: Create the project and add CsvHelper
Open a terminal in the folder where you keep projects and run these commands. The first creates a Razor Pages project, the second moves into the new folder, and the third adds the CSV library.
dotnet new webapp -n CodingvilaCsvImport
cd CodingvilaCsvImport
dotnet add package CsvHelper
dotnet add package records the current
version of CsvHelper in your project file, so you don't have to type a
version number. Then create two folders named
Models and
Services. Your project will look like this
when you finish:
CodingvilaCsvImport/
Models/OrderRow.cs
Services/OrderCsvImporter.cs
Pages/Index.cshtml
Pages/Index.cshtml.cs
Program.cs
Step 2: Create the model
A model describes one CSV record. Using a
record gives you a small, read-only class
with named properties. Create
Models/OrderRow.cs:
namespace CodingvilaCsvImport.Models;
public sealed record OrderRow(
int Id,
string CustomerName,
string Item,
int Qty,
decimal Price,
decimal Amount);
Step 3: Create the CSV importer service
Keeping the parsing code in its own class has two benefits. The page stays
short, and you can reuse or unit test the importer without a web page.
Create Services/OrderCsvImporter.cs.
The importer uses CsvHelper's low-level
CsvParser rather than the more common
CsvReader with
GetRecords<T>().
CsvReader maps columns to a class in one
call, which is shorter when you trust the file.
CsvParser gives you each record as a string
array, so you can validate every value yourself and report the exact row
that's wrong.
using System.Globalization;
using System.Text;
using CodingvilaCsvImport.Models;
using CsvHelper;
using CsvHelper.Configuration;
namespace CodingvilaCsvImport.Services;
public sealed class ImportResult
{
public List<OrderRow> Rows { get; } = new();
public string? Error { get; set; }
public bool Succeeded => Error is null;
}
public sealed class OrderCsvImporter
{
private const int ExpectedColumns = 6;
private const int MaxRows = 10_000;
public async Task<ImportResult> ReadAsync(Stream stream, bool hasHeader)
{
var result = new ImportResult();
var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{
IgnoreBlankLines = true,
TrimOptions = TrimOptions.Trim
};
using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
using var parser = new CsvParser(reader, config);
var headerPending = hasHeader;
var lastRow = 0;
try
{
while (await parser.ReadAsync())
{
lastRow = parser.Row;
// Skip the header row if the user said the file has one
if (headerPending)
{
headerPending = false;
continue;
}
var fields = parser.Record;
if (fields is null)
{
continue;
}
if (result.Rows.Count >= MaxRows)
{
result.Error = $"The file has more than {MaxRows:N0} data rows. Split it into smaller files.";
return result;
}
var (order, error) = ParseRow(fields, parser.Row);
if (order is null)
{
result.Error = error;
return result;
}
result.Rows.Add(order);
}
}
catch (CsvHelperException)
{
result.Error = $"The file is not valid CSV after row {lastRow}. Check for stray or unmatched quotes.";
return result;
}
if (result.Rows.Count == 0)
{
result.Error = "The file contains no data rows.";
}
return result;
}
private static (OrderRow? Row, string? Error) ParseRow(string[] fields, int row)
{
var prefix = $"Row {row}: ";
if (fields.Length != ExpectedColumns)
{
return (null, $"{prefix}expected {ExpectedColumns} values but found {fields.Length}.");
}
if (!int.TryParse(fields[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var id))
{
return (null, $"{prefix}Id must be a whole number.");
}
if (string.IsNullOrWhiteSpace(fields[1]))
{
return (null, $"{prefix}CustomerName is required.");
}
if (!int.TryParse(fields[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out var qty) || qty < 1)
{
return (null, $"{prefix}Qty must be a whole number of 1 or more.");
}
if (!decimal.TryParse(fields[4], NumberStyles.Number, CultureInfo.InvariantCulture, out var price) || price < 0)
{
return (null, $"{prefix}Price must be a number such as 2.50, and not negative.");
}
if (!decimal.TryParse(fields[5], NumberStyles.Number, CultureInfo.InvariantCulture, out var amount) || amount < 0)
{
return (null, $"{prefix}Amount must be a number such as 25.00, and not negative.");
}
return (new OrderRow(id, fields[1], fields[2], qty, price, amount), null);
}
}
Here is what the importer does, step by step:
-
CsvConfiguration(CultureInfo.InvariantCulture)tells CsvHelper to use culture-independent rules, so the result doesn't change with the server's regional settings.IgnoreBlankLinesskips empty lines, andTrimOptions.Trimremoves spaces around values. -
CsvParsersplits each record into astring[]and understands quoted values, so"Mehta, Rahul"stays one field. -
The first record is skipped only when
hasHeaderistrue. -
ParseRowchecks the number of values and converts each one withTryParse. It returns either anOrderRowor a readable error message with the row number. -
The loop stops at 10,000 data rows (
MaxRows) so a huge file can't use up the server's memory. Change that number to fit your needs. -
A
CsvHelperException, such as a stray quote inside an unquoted value, is turned into a friendly message instead of a server error.
One behavior is worth knowing before you test. In CsvHelper, a quoted value
that is never closed does not always throw. Reports from other developers
show that the parser can fold the following lines into that one field, so
the broken record ends up with fewer than six values. In that case
ParseRow catches it and you see "expected 6
values but found 2" on that row instead of an invalid-CSV message. Either
way the import stops with a readable message and nothing is shown.
Row numbers are the parser's record positions. They can differ from the line numbers shown in a text editor if the file contains blank lines or quoted values that span several lines.
Step 4: Register the service
Open Program.cs and add one line before
var app = builder.Build();, plus the
using line at the top. Leave the rest of
the file as the template generated it, because its contents vary slightly
between template versions.
using CodingvilaCsvImport.Services;
// ...the template's existing lines, including builder.Services.AddRazorPages();
builder.Services.AddSingleton<OrderCsvImporter>();
A singleton is safe here because the importer keeps no data between calls.
Each call to ReadAsync creates its own
parser and result.
Step 5: Write the page model
The page model is the equivalent of Web Forms code-behind. Replace the
contents of Pages/Index.cshtml.cs:
using CodingvilaCsvImport.Models;
using CodingvilaCsvImport.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace CodingvilaCsvImport.Pages;
// Reject any request body over 3 MB (2 MB file plus room for the form fields)
[RequestSizeLimit(3 * 1024 * 1024)]
public class IndexModel : PageModel
{
private const long MaxFileBytes = 2 * 1024 * 1024; // 2 MB
private readonly OrderCsvImporter _importer;
public IndexModel(OrderCsvImporter importer)
{
_importer = importer;
}
[BindProperty]
public IFormFile? Upload { get; set; }
[BindProperty]
public bool HasHeader { get; set; }
public IReadOnlyList<OrderRow> Orders { get; private set; } = Array.Empty<OrderRow>();
public void OnGet()
{
}
public async Task<IActionResult> OnPostAsync()
{
// 1. Check that a .csv file was chosen and is not too large
if (Upload is null || Upload.Length == 0 ||
!string.Equals(Path.GetExtension(Upload.FileName), ".csv", StringComparison.OrdinalIgnoreCase))
{
ModelState.AddModelError(nameof(Upload), "Please choose a .csv file.");
return Page();
}
if (Upload.Length > MaxFileBytes)
{
ModelState.AddModelError(nameof(Upload), "The file is too large. The limit is 2 MB.");
return Page();
}
// 2. Parse the uploaded stream (nothing is saved to disk)
await using var stream = Upload.OpenReadStream();
var result = await _importer.ReadAsync(stream, HasHeader);
if (!result.Succeeded)
{
ModelState.AddModelError(nameof(Upload), result.Error!);
return Page();
}
// 3. Hand the rows to the view
Orders = result.Rows;
return Page();
}
}
A few points about this class:
-
[BindProperty]tells ASP.NET Core to fillUploadandHasHeaderfrom the submitted form.IFormFilerepresents the uploaded file. The?inIFormFile?matters: without it, nullable reference types make ASP.NET Core treat the property as required and add its own validation error. -
OnPostAsyncruns when the form is submitted. Validation messages are added toModelState, and the page shows them in a summary. -
The handler never uses
Upload.FileNameas a path. It reads only the extension, and the file is read straight from the stream. -
[RequestSizeLimit]rejects oversized requests before your handler runs. The file check inside the handler is a second, friendlier layer.
Step 6: Write the Razor view
Replace the contents of Pages/Index.cshtml.
The class names such as form-control and
table come from Bootstrap, which the
default webapp template includes. If you
removed Bootstrap, the page still works but looks plain.
@page
@model IndexModel
@{
ViewData["Title"] = "Codingvila - Import Orders from CSV";
}
<h1>Import orders from a CSV file</h1>
<form method="post" enctype="multipart/form-data">
<div class="mb-3">
<label asp-for="Upload" class="form-label">CSV file</label>
<input asp-for="Upload" type="file" accept=".csv,text/csv" class="form-control" />
</div>
<div class="form-check mb-3">
<input asp-for="HasHeader" class="form-check-input" />
<label asp-for="HasHeader" class="form-check-label">First row contains column names</label>
</div>
<button type="submit" class="btn btn-primary">Import</button>
<div asp-validation-summary="All" class="text-danger mt-3"></div>
</form>
@if (Model.Orders.Count > 0)
{
<p class="mt-4">Imported @Model.Orders.Count rows.</p>
<table class="table table-striped">
<thead>
<tr>
<th>Id</th>
<th>CustomerName</th>
<th>Item</th>
<th>Qty</th>
<th>Price</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
@foreach (var order in Model.Orders)
{
<tr>
<td>@order.Id</td>
<td>@order.CustomerName</td>
<td>@order.Item</td>
<td>@order.Qty</td>
<td>@order.Price.ToString("N2")</td>
<td>@order.Amount.ToString("N2")</td>
</tr>
}
</tbody>
</table>
}
What to notice in the view:
-
enctype="multipart/form-data"is required for file uploads. Without it, the browser sends only the file name andUploadstaysnull. - The form tag helper adds an anti-forgery token to POST forms automatically, and Razor Pages checks that token by default. That protects the upload against cross-site request forgery.
-
Razor HTML-encodes anything you output with
@, so a value like<script>in the CSV appears as plain text. -
The
accept=".csv,text/csv"attribute only filters the file picker in the browser. It is not validation, so the server-side checks in the page model are still required. -
ToString("N2")shows prices with two decimals and thousands separators, using the server's current culture. Your output can use different separators on a server with another regional setting.
Step 7: Run and test the page
Start the app from the project folder:
dotnet run
The terminal prints a line that starts with "Now listening on:" followed by
a local address such as
http://localhost:5123. The port number is
different on every new project, so use the one the terminal shows. Open that
address, choose codingvila-orders.csv,
leave the header checkbox cleared, and click Import. You
should see these four rows, with
Mehta, Rahul in one cell:
| Id | CustomerName | Item | Qty | Price | Amount |
|---|---|---|---|---|---|
| 1 | Maria Gonzalez | Notebook | 10 | 2.50 | 25.00 |
| 2 | James Carter | USB cable | 3 | 7.99 | 23.97 |
| 3 | Aisha Patel | Desk lamp | 1 | 24.00 | 24.00 |
| 4 | Mehta, Rahul | Stapler | 2 | 5.25 | 10.50 |
The quoted comma stayed inside the customer name, which is the point of using a real CSV parser. The exact number format on screen can differ if your server uses another regional setting.
Then try some error cases: no file, a
.txt file, a line with a missing value, a
word or 0 in the
Qty column, a missing closing quote, and a
header row with the checkbox cleared. Messages use the word "Row" instead of
"Line", for example "Row 2: expected 6 values but found 5."
Step 8: Test the importer (optional but recommended)
Because the importer is its own class, you can test it without starting a web server. These two xUnit tests check that a quoted comma stays in one field and that a bad quantity produces a readable message. Create the test project next to your web project and reference it:
dotnet new xunit -n CodingvilaCsvImport.Tests
dotnet add CodingvilaCsvImport.Tests reference CodingvilaCsvImport
dotnet add CodingvilaCsvImport.Tests package CsvHelper
Then add a file such as
OrderCsvImporterTests.cs to the test
project and run dotnet test:
using System.Text;
using CodingvilaCsvImport.Services;
using Xunit;
public class OrderCsvImporterTests
{
private static MemoryStream ToStream(string csv) => new(Encoding.UTF8.GetBytes(csv));
[Fact]
public async Task Quoted_comma_stays_in_one_field()
{
var csv = "4,\"Mehta, Rahul\",Stapler,2,5.25,10.50";
var result = await new OrderCsvImporter().ReadAsync(ToStream(csv), hasHeader: false);
Assert.True(result.Succeeded);
Assert.Equal("Mehta, Rahul", Assert.Single(result.Rows).CustomerName);
}
[Fact]
public async Task Bad_quantity_returns_a_readable_error()
{
var csv = "1,Maria Gonzalez,Notebook,ten,2.50,25.00";
var result = await new OrderCsvImporter().ReadAsync(ToStream(csv), hasHeader: false);
Assert.False(result.Succeeded);
Assert.Contains("Qty must be", result.Error);
}
}
The test project needs a reference to the web project, which is why the second command is there. The extra CsvHelper reference is only a safeguard in case your project does not flow the dependency through. You can remove it if the build works without it.
Security and production notes for ASP.NET Core
-
Require sign-in. Add
[Authorize]to the page model (it needsusing Microsoft.AspNetCore.Authorization;) and configure authentication inProgram.csbefore using this with real data. - Keep the anti-forgery protection. Razor Pages validates the token for POST requests by default. Don't disable it for an upload page.
-
Know your size limits. Kestrel's default request body
limit is about 30 MB (30,000,000 bytes).
[RequestSizeLimit]lowers it for this page. If you host behind IIS or another proxy, that server has its own limit too. -
Remember that
IFormFilebuffers the upload. ASP.NET Core reads the whole request before your handler runs, keeping a file in memory up to 64 KB by default and spilling anything larger to a temporary file on disk. That's why a small limit matters. For very large files, use streaming techniques instead ofIFormFile. - Don't use the client's file name as a path. If you ever save the file, generate your own name and store it outside the web root.
-
Save to a database safely. With Entity Framework Core or
ADO.NET, use parameters or
SqlBulkCopy, never string-built SQL. Because the importer returns typed objects, it's easy to hand them to either one. -
Beware spreadsheet formulas on export. If you write
imported text back to a CSV that opens in Excel, sanitize values that
start with
=,+,-, or@.
Common ASP.NET Core errors and how to fix them
"The type or namespace name 'CsvHelper' could not be found."
The package isn't installed in this project. Run
dotnet add package CsvHelper in the folder
that contains the .csproj file.
"Please choose a .csv file." even though you chose one.
Check that the form has
enctype="multipart/form-data" and that the
input's asp-for matches the property name
Upload.
HTTP 400 Bad Request when posting from a tool such as curl or Postman. The anti-forgery token is missing. Browsers submitting the page's own form send it automatically. For a script or API client, build an endpoint designed for that purpose rather than turning off protection on this page.
HTTP 413 or a "request body too large" error. The request
is larger than [RequestSizeLimit] or the
server's limit. Use a smaller file, or raise the limit deliberately and
raise MaxFileBytes to match.
"The file is not valid CSV after row N." A quote is in the wrong place, near the record after row N. Open the file in a text editor and fix it.
"Row N: expected 6 values but found 2" (or another small number). This is often the other face of a missing closing quote: the unclosed quote swallows the lines after it into one field. Check the quotes on row N.
Accented characters look wrong or every row fails. The
importer reads the file as UTF-8 and honors a byte-order mark if there is
one. A file saved from Excel as plain "CSV (Comma delimited)" may use a
legacy code page, so save it as "CSV UTF-8" instead. Some regional Excel
settings also write semicolons instead of commas. If your files use
semicolons, set the Delimiter property on
CsvConfiguration to
";".
Other ways to build the same feature in ASP.NET Core
Razor Pages is the closest match to a Web Forms page, but the importer class
doesn't depend on it. You can call
OrderCsvImporter.ReadAsync from an MVC
controller action that accepts an
IFormFile, or from a minimal API endpoint,
and return the rows as JSON. Only the code that receives the file and shows
the result changes.
Part 2: Read a CSV file in ASP.NET Web Forms and show it in a GridView (.NET Framework)
Before you start
- A Windows computer with Visual Studio and the ASP.NET and web development workload installed. Menu and dialog names differ slightly between Visual Studio versions.
-
The .NET Framework 4.8 is the recommended target. The code only needs APIs
that exist since .NET Framework 4.0 (
string.IsNullOrWhiteSpaceandTextFieldParseramong them). According to Microsoft's .NET Framework download page, versions 4.6.1 and older are already out of support, and 4.6.2 reaches end of support on January 12, 2027. - Basic knowledge of C# or VB.NET.
-
The sample file
codingvila-orders.csvfrom the previous section.
Note: ASP.NET Web Forms runs on the .NET Framework only. Microsoft has stated that Web Forms will not be added to .NET Core, so this page and its controls don't exist in ASP.NET Core. The .NET Framework 4.8 continues to be supported when it's installed on a supported version of Windows. If you're starting a new project, use Part 1 instead.
What you will build
- A web page with a file upload control, a header-row checkbox, an Import button, a message label, and a grid.
- A button click method that reads the uploaded CSV file and validates every record.
-
Code that loads the valid values into a
DataTableand shows them in theGridView.
Implementation
First you need an ASP.NET web page. Follow these steps to create an empty web site and add a page to it.
Step 1: Open Visual Studio.
Step 2: From the top menu, click "FILE" >> "NEW" >> "WEB SITE". If you don't see that menu item, create an ASP.NET Web Application (.NET Framework) project with the Empty template and the Web Forms option ticked. Everything below works the same way in that project type.
Step 3: A dialog opens. Pick your language, C# or VB.NET. I chose C# here.
Select "ASP.NET Empty Web Site", choose the folder where the site should be saved, and click OK. You now have an empty web site.
Next, add a web page to the project's root folder.
Step 4: Right-click the project name in Solution Explorer and click "Add" >> "Add New Item".
Step 5: Select Web Form, name the page
CS (the markup below refers to
CS.aspx), make sure
Place code in separate file is ticked, and click
Add. That option creates the code-behind file (CS.aspx.cs
or CS.aspx.vb) used later in this article.
Now replace the contents of CS.aspx with
the markup below. It adds the controls: a
FileUpload, a
CheckBox for the header row, a
Button, a
Label for messages, and a
GridView. The small
<style> block gives the grid a pink
header row and the Import button a blue background.
ASPX markup
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="CS.aspx.cs" Inherits="CS" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Codingvila - Import Orders from CSV</title>
<style type="text/css">
body
{
font-family: Arial;
font-size: 10pt;
}
table
{
border-collapse: collapse;
background-color: #fff;
}
table th
{
background-color: #ff0097;
color: #fff;
font-weight: bold;
}
table th, table td
{
padding: 5px;
border: 1px solid #ccc;
}
.button
{
background-color: #223c88; /* Blue */
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
}
.error
{
color: #c00;
}
</style>
</head>
<body>
<form id="form1" runat="server">
<asp:FileUpload ID="FileUpload1" runat="server" />
<asp:CheckBox ID="chkHasHeader" Text="First row contains column names" runat="server" />
<asp:Button ID="btnRead" CssClass="button" runat="server" Text="Import" OnClick="ReadCSV" />
<asp:Label ID="lblMessage" CssClass="error" runat="server"></asp:Label>
<hr />
<asp:GridView ID="GridView1" EnableViewState="false" runat="server">
</asp:GridView>
</form>
</body>
</html>
The button's OnClick="ReadCSV" points to a
method you haven't written yet, so don't run the page until the next step is
done. Running it first causes an error.
EnableViewState="false" on the grid keeps
the imported rows out of the page's hidden view state, so the page stays
small. The grid is rebuilt on every click of Import, so nothing is lost.
If you chose VB.NET: keep the
Language,
CodeFile, and
Inherits values that Visual Studio
generated for your page. They will say
VB and point to a
.aspx.vb file instead of the C# values
shown above. In a Web Application project (not a Web Site), the attribute is
CodeBehind instead of
CodeFile. Leave whatever Visual Studio
generated.
Code-behind: read and validate the CSV file
Add the following code to the code-behind file (CS.aspx.cs
or the .aspx.vb file). The namespaces at
the top matter: System.IO provides
Path,
System.Data provides
DataTable,
System.Globalization provides
CultureInfo, and
Microsoft.VisualBasic.FileIO provides
TextFieldParser.
The code is split into three parts.
ReadCSV is the click handler.
CreateOrdersTable builds the empty table.
TryAddRow validates one record and adds it.
C#
using System;
using System.Data;
using System.Globalization;
using System.IO;
using System.Text;
using Microsoft.VisualBasic.FileIO;
public partial class CS : System.Web.UI.Page
{
private const int ExpectedColumns = 6;
private const int MaxFileSizeBytes = 2 * 1024 * 1024; // 2 MB
protected void ReadCSV(object sender, EventArgs e)
{
lblMessage.Text = string.Empty;
GridView1.DataSource = null;
GridView1.DataBind();
// 1. Check that a .csv file was chosen and is not too large
if (!FileUpload1.HasFile ||
!string.Equals(Path.GetExtension(FileUpload1.FileName), ".csv", StringComparison.OrdinalIgnoreCase))
{
lblMessage.Text = "Please choose a .csv file.";
return;
}
if (FileUpload1.PostedFile.ContentLength > MaxFileSizeBytes)
{
lblMessage.Text = "The file is too large. The limit is 2 MB.";
return;
}
// 2. Parse the uploaded stream (nothing is saved to disk)
DataTable dt = CreateOrdersTable();
string error;
try
{
using (TextFieldParser parser = new TextFieldParser(FileUpload1.PostedFile.InputStream, Encoding.UTF8))
{
parser.TextFieldType = FieldType.Delimited;
parser.SetDelimiters(",");
parser.HasFieldsEnclosedInQuotes = true;
// Skip the header row if the user ticked the checkbox
if (chkHasHeader.Checked && !parser.EndOfData)
{
parser.ReadFields();
}
while (!parser.EndOfData)
{
long lineNumber = parser.LineNumber;
string[] fields = parser.ReadFields();
if (!TryAddRow(dt, fields, lineNumber, out error))
{
lblMessage.Text = error;
return;
}
}
}
}
catch (MalformedLineException ex)
{
lblMessage.Text = "Line " + ex.LineNumber + " is not valid CSV. Check for a missing closing quote.";
return;
}
if (dt.Rows.Count == 0)
{
lblMessage.Text = "The file contains no data rows.";
return;
}
// 3. Bind the DataTable to the GridView
GridView1.DataSource = dt;
GridView1.DataBind();
}
private static DataTable CreateOrdersTable()
{
DataTable dt = new DataTable();
dt.Columns.Add("Id", typeof(int));
dt.Columns.Add("CustomerName", typeof(string));
dt.Columns.Add("Item", typeof(string));
dt.Columns.Add("Qty", typeof(int));
dt.Columns.Add("Price", typeof(decimal));
dt.Columns.Add("Amount", typeof(decimal));
return dt;
}
private static bool TryAddRow(DataTable dt, string[] fields, long lineNumber, out string error)
{
error = null;
string prefix = "Line " + lineNumber + ": ";
if (fields.Length != ExpectedColumns)
{
error = prefix + "expected " + ExpectedColumns + " values but found " + fields.Length + ".";
return false;
}
int id, qty;
decimal price, amount;
if (!int.TryParse(fields[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out id))
{
error = prefix + "Id must be a whole number.";
return false;
}
if (string.IsNullOrWhiteSpace(fields[1]))
{
error = prefix + "CustomerName is required.";
return false;
}
if (!int.TryParse(fields[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out qty) || qty < 1)
{
error = prefix + "Qty must be a whole number of 1 or more.";
return false;
}
if (!decimal.TryParse(fields[4], NumberStyles.Number, CultureInfo.InvariantCulture, out price) || price < 0)
{
error = prefix + "Price must be a number such as 2.50, and not negative.";
return false;
}
if (!decimal.TryParse(fields[5], NumberStyles.Number, CultureInfo.InvariantCulture, out amount) || amount < 0)
{
error = prefix + "Amount must be a number such as 25.00, and not negative.";
return false;
}
dt.Rows.Add(id, fields[1], fields[2], qty, price, amount);
return true;
}
}
VB.NET
Imports System.Data
Imports System.Globalization
Imports System.IO
Imports System.Text
Imports Microsoft.VisualBasic.FileIO
Partial Class CS
Inherits System.Web.UI.Page
Private Const ExpectedColumns As Integer = 6
Private Const MaxFileSizeBytes As Integer = 2 * 1024 * 1024 ' 2 MB
Protected Sub ReadCSV(sender As Object, e As EventArgs)
lblMessage.Text = String.Empty
GridView1.DataSource = Nothing
GridView1.DataBind()
'1. Check that a .csv file was chosen and is not too large
If Not FileUpload1.HasFile OrElse
Not String.Equals(Path.GetExtension(FileUpload1.FileName), ".csv", StringComparison.OrdinalIgnoreCase) Then
lblMessage.Text = "Please choose a .csv file."
Return
End If
If FileUpload1.PostedFile.ContentLength > MaxFileSizeBytes Then
lblMessage.Text = "The file is too large. The limit is 2 MB."
Return
End If
'2. Parse the uploaded stream (nothing is saved to disk)
Dim dt As DataTable = CreateOrdersTable()
Dim errorMessage As String = Nothing
Try
Using parser As New TextFieldParser(FileUpload1.PostedFile.InputStream, Encoding.UTF8)
parser.TextFieldType = FieldType.Delimited
parser.SetDelimiters(",")
parser.HasFieldsEnclosedInQuotes = True
'Skip the header row if the user ticked the checkbox
If chkHasHeader.Checked AndAlso Not parser.EndOfData Then
parser.ReadFields()
End If
Do While Not parser.EndOfData
Dim lineNumber As Long = parser.LineNumber
Dim fields As String() = parser.ReadFields()
If Not TryAddRow(dt, fields, lineNumber, errorMessage) Then
lblMessage.Text = errorMessage
Return
End If
Loop
End Using
Catch ex As MalformedLineException
lblMessage.Text = "Line " & ex.LineNumber & " is not valid CSV. Check for a missing closing quote."
Return
End Try
If dt.Rows.Count = 0 Then
lblMessage.Text = "The file contains no data rows."
Return
End If
'3. Bind the DataTable to the GridView
GridView1.DataSource = dt
GridView1.DataBind()
End Sub
Private Shared Function CreateOrdersTable() As DataTable
Dim dt As New DataTable()
dt.Columns.Add("Id", GetType(Integer))
dt.Columns.Add("CustomerName", GetType(String))
dt.Columns.Add("Item", GetType(String))
dt.Columns.Add("Qty", GetType(Integer))
dt.Columns.Add("Price", GetType(Decimal))
dt.Columns.Add("Amount", GetType(Decimal))
Return dt
End Function
Private Shared Function TryAddRow(dt As DataTable, fields As String(), lineNumber As Long, ByRef errorMessage As String) As Boolean
errorMessage = Nothing
Dim prefix As String = "Line " & lineNumber & ": "
If fields.Length <> ExpectedColumns Then
errorMessage = prefix & "expected " & ExpectedColumns & " values but found " & fields.Length & "."
Return False
End If
Dim id As Integer
Dim qty As Integer
Dim price As Decimal
Dim amount As Decimal
If Not Integer.TryParse(fields(0), NumberStyles.Integer, CultureInfo.InvariantCulture, id) Then
errorMessage = prefix & "Id must be a whole number."
Return False
End If
If String.IsNullOrWhiteSpace(fields(1)) Then
errorMessage = prefix & "CustomerName is required."
Return False
End If
If Not Integer.TryParse(fields(3), NumberStyles.Integer, CultureInfo.InvariantCulture, qty) OrElse qty < 1 Then
errorMessage = prefix & "Qty must be a whole number of 1 or more."
Return False
End If
If Not Decimal.TryParse(fields(4), NumberStyles.Number, CultureInfo.InvariantCulture, price) OrElse price < 0 Then
errorMessage = prefix & "Price must be a number such as 2.50, and not negative."
Return False
End If
If Not Decimal.TryParse(fields(5), NumberStyles.Number, CultureInfo.InvariantCulture, amount) OrElse amount < 0 Then
errorMessage = prefix & "Amount must be a number such as 25.00, and not negative."
Return False
End If
dt.Rows.Add(id, fields(1), fields(2), qty, price, amount)
Return True
End Function
End Class
C# only: if the compiler reports that the namespace
VisualBasic does not exist in
Microsoft, your project doesn't reference
the assembly yet. In a Web Site project, add it to
Web.config. Keep the
targetFramework value your project already
has.
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.8">
<assemblies>
<add assembly="Microsoft.VisualBasic, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
</assemblies>
</compilation>
</system.web>
</configuration>
In a Web Application project, use Add Reference >>
Assemblies >> Framework and tick
Microsoft.VisualBasic instead. VB.NET
projects reference it automatically.
Now run the page (press F5), choose
codingvila-orders.csv, and click
Import.
How the Web Forms code works
The flowchart below shows what happens when you click
Import. The code first confirms that a
.csv file of 2 MB or less was chosen. It
then creates an empty DataTable and skips
the header row if the checkbox is ticked.
After that it reads one record at a time and validates each one before
adding it as a row. If a record is wrong, or the parser finds malformed
quotes, the code shows a message and stops. Otherwise, once the last record
is read, the table is bound to the grid. To keep the chart readable, it
leaves out one small check: if the file has no data rows at all, the page
shows "The file contains no data rows." instead of an empty grid. The
ASP.NET Core version in Part 1 follows the same logic, with a list of
objects and an HTML table instead of a
DataTable and a
GridView.
A few details are worth knowing:
-
FileUpload1.HasFileisfalsewhen the user clicks Import without choosing a file, so checking it prevents a crash. Checking the extension is a basic safety check, not proof that the content is valid CSV. -
The 2 MB check runs after ASP.NET has already received the request. The
setting that actually stops oversized uploads early is
maxRequestLength, described in the troubleshooting section below. -
TextFieldParserreads the upload stream directly. WithHasFieldsEnclosedInQuotesset totrue, a quoted value such as"Mehta, Rahul"stays in one field. The parser also skips blank lines, trims spaces around values by default, and throws aMalformedLineExceptionwhen quotes don't close. -
Each value is converted with
TryParseandCultureInfo.InvariantCulture. That means2.50is always read as two and a half, whatever the server's regional settings are, and a bad value produces a readable message instead of an exception. -
Nothing is saved to the server's disk, so there's no folder to create and
no risk of one user overwriting another user's file. If you do need to
keep the file, validate it first, generate your own file name, and save it
in
App_Dataor another folder that isn't served to browsers. -
The parser handles one record at a time, but this example still loads
every valid record into one
DataTable. That's fine for the 2 MB limit used here. For much larger files, process or insert the records in batches instead of showing them all in a grid. -
The
Amountcolumn is read as given and isn't checked againstQtytimesPrice. If your business rules require that, add the comparison insideTryAddRow.
Output
With the sample file and the checkbox cleared, the
GridView shows one row per line of the
file. The column headings are Id,
CustomerName,
Item, Qty,
Price, and
Amount. You should see these four rows:
| Id | CustomerName | Item | Qty | Price | Amount |
|---|---|---|---|---|---|
| 1 | Maria Gonzalez | Notebook | 10 | 2.50 | 25.00 |
| 2 | James Carter | USB cable | 3 | 7.99 | 23.97 |
| 3 | Aisha Patel | Desk lamp | 1 | 24.00 | 24.00 |
| 4 | Mehta, Rahul | Stapler | 2 | 5.25 | 10.50 |
Row 4 shows that the quoted comma stayed inside the customer name. The grid shows decimal numbers using the server's culture settings, so the exact number format on screen can differ from this table.
Test the error handling
Try these quick tests to confirm the page behaves as described. Each one should show a red message next to the button and leave the grid empty.
- Click Import without choosing a file. You should see "Please choose a .csv file."
-
Choose a
.txtfile. You should see the same message. - Remove one value from a line. You should see a message such as "Line 2: expected 6 values but found 5."
-
Replace the quantity on a line with a word, or with
0. You should see "Qty must be a whole number of 1 or more." -
Delete the closing quote from
"Mehta, Rahul". You should see a message that the line is not valid CSV, or a wrong-column-count message. Which one appears depends on how the parser reads the unclosed quote. -
Tick the header checkbox when the file has no header. The first data row
is skipped, so one row will be missing. Untick it when the file does have
a header, and you'll get an error about the
Idvalue.
Security and production notes for Web Forms
This page is a good starting point, but treat it as a demo until you've reviewed these points for your own site.
- Require sign-in. The page lets anyone who can open it upload a file. Put it behind your site's authentication and authorization before using it with real data.
-
Keep the size limit. The code checks
ContentLengthagainst 2 MB. ASP.NET also enforces its own limit, described in the troubleshooting section, and IIS has a separate one. - Don't trust the file name or extension. The extension check only filters out obvious mistakes. This code never saves the file, which avoids the biggest risks of file uploads.
-
Treat every value as untrusted text. If you define your
own
BoundFieldcolumns, leaveHtmlEncodeat its default oftrue. If you render a value yourself in a template, use the encoding expression<%#: Eval("CustomerName") %>(.NET Framework 4.5 and later), so text such as<script>in a CSV shows up as plain text instead of running. -
Beware spreadsheet formulas on export. If you ever write
imported values back to a CSV that people open in Excel, a value that
starts with
=,+,-, or@can be run as a formula. Sanitize those values on export. -
Save to a database with parameters. If you insert the
rows into SQL Server, use parameterized commands or
SqlBulkCopy. Never build SQL text by joining CSV values. -
Don't put the upload inside an UpdatePanel.
FileUploaddoesn't work with asynchronous partial postbacks, soHasFilewould always befalse. If the page uses an UpdatePanel, register the Import button as aPostBackTriggerso it does a full postback.
Common Web Forms errors and how to fix them
"Please choose a .csv file." You either didn't select a
file, or the file name doesn't end in .csv.
Choose a file with the right extension and try again.
"Line N: expected 6 values but found X." That line has too few or too many fields. Check for a missing value or an extra comma. If a value legitimately contains a comma, it must be wrapped in double quotes in the file.
"Line N is not valid CSV." A quote is in the wrong place or a quoted value is never closed. Open the file in a text editor, go to that line, and fix the quotes.
An error about Id on line 1.
Your file has a header row, but the checkbox is cleared. Tick
First row contains column names and import again.
Accented characters look wrong. The code reads the file as UTF-8, and it also honors a byte-order mark if the file has one. If you saved the file from Excel as plain "CSV (Comma delimited)", Excel may use a legacy code page instead. Save it as "CSV UTF-8" if your Excel version offers that option, or save it from a text editor as UTF-8.
Every row fails with the wrong number of values. Your file
may use semicolons instead of commas. Some regional Excel settings do this.
Either re-save the file with commas, or change
SetDelimiters(",") to match your delimiter.
Prices show or parse differently on another server. The
code reads numbers with
CultureInfo.InvariantCulture, so a value
such as 2.50 always parses the same way.
Only the display depends on the server's culture. A file that uses a comma
as the decimal mark, such as 2,50, needs
quotes around the value and a different parsing culture.
"The type or namespace name 'VisualBasic' does not exist" (C#
only).
Add the Microsoft.VisualBasic reference as
shown in the code-behind section.
"Maximum request length exceeded." ASP.NET limits upload
size, and the default is 4 MB (4096 KB). IIS has its own limit,
maxAllowedContentLength, which defaults to
about 30 MB (30,000,000 bytes). To allow uploads of up to 10 MB, set both
limits in Web.config as shown below, and
raise MaxFileSizeBytes in the code to
match. This error happens before your code runs, so your page can't show a
friendly message for it. Raise limits only as far as you need, because
larger limits make it easier to overload your server.
<configuration>
<system.web>
<!-- maxRequestLength is in kilobytes: 10240 KB = 10 MB -->
<httpRuntime maxRequestLength="10240" />
</system.web>
<system.webServer>
<security>
<requestFiltering>
<!-- maxAllowedContentLength is in bytes: 10485760 bytes = 10 MB -->
<requestLimits maxAllowedContentLength="10485760" />
</requestFiltering>
</security>
</system.webServer>
</configuration>
Which CSV parsing approach should you use?
You have three common options. The right one depends on how messy your files can be and which platform you use.
| Approach | Handles quoted commas | Extra dependency | Best for |
|---|---|---|---|
string.Split(',') |
No | None | Files you create yourself, with no commas inside values |
TextFieldParser |
Yes | None (part of Microsoft.VisualBasic, which ships with the .NET Framework and with modern .NET) | Web Forms imports on the .NET Framework, as in Part 2 |
| CsvHelper (NuGet package) | Yes | One NuGet package | ASP.NET Core projects, as in Part 1, and mapping rows to classes. For a .NET Framework project, check the package's supported target frameworks first. |
Frequently asked questions
How do I read a CSV file in ASP.NET using C#?
In ASP.NET Core, accept an IFormFile, read
its stream with CsvHelper's CsvParser, and
validate each record. In Web Forms, use a
FileUpload control, wrap its
PostedFile.InputStream in a
TextFieldParser, and read records with
ReadFields(). In both cases, add the valid
records to a table or list and display it.
How do I read a CSV file in ASP.NET Core?
Create a Razor Page with a form that has
enctype="multipart/form-data" and an
IFormFile property. In
OnPostAsync, call
Upload.OpenReadStream() and pass the stream
to a CSV parser such as CsvHelper. Part 1 of this article shows the complete
code.
Which .NET version should I use for a new project?
As of October 2026, .NET 10 is the current LTS release, and Microsoft says it is supported through November 2028. .NET 8 and .NET 9 reach end of support on November 10, 2026. .NET 11 is expected in November 2026 as a standard-term support release. Check Microsoft's .NET support policy page for current dates.
How do I upload and read a CSV file in ASP.NET MVC?
In ASP.NET Core MVC, use an action that takes an
IFormFile and call the importer from Part
1. In classic ASP.NET MVC 5 on the .NET Framework, the action takes an
HttpPostedFileBase. Pass its
InputStream to
TextFieldParser exactly as the Web Forms
code in Part 2 does. In both cases the form needs
enctype="multipart/form-data".
How do I read a CSV file into a DataTable in C#?
Create the DataTable, add typed columns,
and add one row per validated record, as the Web Forms code in Part 2 does.
If you use CsvHelper, its
CsvDataReader class can also feed a
DataTable through
DataTable.Load, but that skips the per-row
validation messages this article builds.
How do I display CSV data in an HTML table or GridView?
Parse and validate the file first, then show the rows. In Razor Pages, loop
over a list of objects and write one
<tr> per record (Part 1, Step 6). In
Web Forms, bind the DataTable to a
GridView (Part 2).
Do I have to save the uploaded CSV file on the server?
No. Both parts read the file straight from the upload stream. That avoids missing-folder errors and file-name collisions, and it keeps user files out of your web root.
How do I read a CSV file from a folder on the server instead of an upload?
Open the file and give the stream to the same parsing code. In ASP.NET Core,
call File.OpenRead(path) and pass the
result to OrderCsvImporter.ReadAsync. In
Web Forms, pass the file path (or a
FileStream) to the
TextFieldParser constructor in place of the
upload stream. Build the path on the server yourself. Never take it from
user input.
How do I skip the header row?
In Web Forms, call parser.ReadFields() once
before the read loop and ignore the result. In ASP.NET Core, skip the first
record the parser returns. Both pages in this article do that when the
header checkbox is ticked.
Why not just use Split(',')?
It works only when no value contains a comma, a quote, or a line break. As
soon as a name like "Mehta, Rahul" appears,
the split produces an extra field and the row is wrong. A real CSV parser
handles quoted values for you.
Can I use the Web Forms code in ASP.NET Core?
No. ASP.NET Web Forms isn't available in ASP.NET Core, so the page, the controls, and the code-behind don't carry over. Use Part 1 for ASP.NET Core. The validation approach is the same, and the importer class in Part 1 contains the logic you'd reuse.
Summary
In ASP.NET Core on .NET 10, you can read a CSV file with
IFormFile, CsvHelper, and a Razor Page. In
ASP.NET Web Forms, you can do it with a
FileUpload control,
TextFieldParser, and a
DataTable bound to a
GridView. The main points to remember:
- Check that a file was chosen, its extension, and its size before reading.
- Read the upload stream directly. You don't need to save the file.
- Use a real CSV parser so quoted commas don't break your rows.
-
Validate each record, parse numbers with
CultureInfo.InvariantCulture, and report the row number when something is wrong. - Web Forms runs on the .NET Framework only. Use ASP.NET Core for new projects.
Related reading
- How to Export a DataTable to CSV in C# and VB.NET
- Export a List to CSV in C# (CsvHelper and StreamWriter Examples)
