Read CSV File in ASP.NET Core & Web Forms (C#, VB.NET)

2 Comments

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:

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.

Read CSV File in ASP.NET Core & Web Forms (C#, VB.NET)

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.

CSV
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

  1. The .NET 10 SDK. Run dotnet --version in a terminal to check what you have.
  2. Any editor: Visual Studio, Visual Studio Code, or a plain text editor plus the dotnet command line.
  3. Basic C# knowledge.
  4. The sample file codingvila-orders.csv from 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.

Flowchart of the Import button logic The user clicks Import. If the file is not a .csv file or is larger than 2 MB, a message is shown and the process stops. Otherwise a DataTable with 6 columns is created and the header row is skipped if the checkbox is ticked. The code then reads the next CSV record. If the record does not have 6 valid values, or the parser finds malformed quotes, a message is shown and the process stops. Otherwise the record is added as a row. While more records remain, the process returns to reading the next record. When no records remain, the DataTable is bound to GridView1 and the grid shows the rows. User clicks Import Valid .csv,2 MB or less? Create DataTable,skip header if ticked Show messageand stop Read nextCSV record 6 validvalues? Add recordas a row Morerecords? Show messageand stop Bind DataTableto GridView1 Grid shows rows Yes No Yes No No Yes, next record
Flowchart of the Import button: validate the file, build the DataTable, check each record, then bind the table to GridView1.

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.

Bash
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:

Text
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:

C#
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.

C#
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:

  1. CsvConfiguration(CultureInfo.InvariantCulture) tells CsvHelper to use culture-independent rules, so the result doesn't change with the server's regional settings. IgnoreBlankLines skips empty lines, and TrimOptions.Trim removes spaces around values.
  2. CsvParser splits each record into a string[] and understands quoted values, so "Mehta, Rahul" stays one field.
  3. The first record is skipped only when hasHeader is true.
  4. ParseRow checks the number of values and converts each one with TryParse. It returns either an OrderRow or a readable error message with the row number.
  5. 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.
  6. 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.

C#
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:

C#
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:

  1. [BindProperty] tells ASP.NET Core to fill Upload and HasHeader from the submitted form. IFormFile represents the uploaded file. The ? in IFormFile? matters: without it, nullable reference types make ASP.NET Core treat the property as required and add its own validation error.
  2. OnPostAsync runs when the form is submitted. Validation messages are added to ModelState, and the page shows them in a summary.
  3. The handler never uses Upload.FileName as a path. It reads only the extension, and the file is read straight from the stream.
  4. [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.

Razor
@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:

  1. enctype="multipart/form-data" is required for file uploads. Without it, the browser sends only the file name and Upload stays null.
  2. 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.
  3. Razor HTML-encodes anything you output with @, so a value like <script> in the CSV appears as plain text.
  4. 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.
  5. 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:

Bash
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:

Bash
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:

C#
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

  1. Require sign-in. Add [Authorize] to the page model (it needs using Microsoft.AspNetCore.Authorization;) and configure authentication in Program.cs before using this with real data.
  2. Keep the anti-forgery protection. Razor Pages validates the token for POST requests by default. Don't disable it for an upload page.
  3. 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.
  4. Remember that IFormFile buffers 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 of IFormFile.
  5. 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.
  6. 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.
  7. 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

  1. 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.
  2. The .NET Framework 4.8 is the recommended target. The code only needs APIs that exist since .NET Framework 4.0 (string.IsNullOrWhiteSpace and TextFieldParser among 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.
  3. Basic knowledge of C# or VB.NET.
  4. The sample file codingvila-orders.csv from 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

  1. A web page with a file upload control, a header-row checkbox, an Import button, a message label, and a grid.
  2. A button click method that reads the uploaded CSV file and validates every record.
  3. Code that loads the valid values into a DataTable and shows them in the GridView.

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

ASPX
<%@ 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#

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

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.

XML
<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.

Flowchart of the Import button logic The user clicks Import. If the file is not a .csv file or is larger than 2 MB, a message is shown and the process stops. Otherwise a DataTable with 6 columns is created and the header row is skipped if the checkbox is ticked. The code then reads the next CSV record. If the record does not have 6 valid values, or the parser finds malformed quotes, a message is shown and the process stops. Otherwise the record is added as a row. While more records remain, the process returns to reading the next record. When no records remain, the DataTable is bound to GridView1 and the grid shows the rows. User clicks Import Valid .csv,2 MB or less? Create DataTable,skip header if ticked Show messageand stop Read nextCSV record 6 validvalues? Add recordas a row Morerecords? Show messageand stop Bind DataTableto GridView1 Grid shows rows Yes No Yes No No Yes, next record
Flowchart of the Import button: validate the file, build the DataTable, check each record, then bind the table to GridView1.

A few details are worth knowing:

  1. FileUpload1.HasFile is false when 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.
  2. 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.
  3. TextFieldParser reads the upload stream directly. With HasFieldsEnclosedInQuotes set to true, 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 a MalformedLineException when quotes don't close.
  4. Each value is converted with TryParse and CultureInfo.InvariantCulture. That means 2.50 is 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.
  5. 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_Data or another folder that isn't served to browsers.
  6. 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.
  7. The Amount column is read as given and isn't checked against Qty times Price. If your business rules require that, add the comparison inside TryAddRow.

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.

  1. Click Import without choosing a file. You should see "Please choose a .csv file."
  2. Choose a .txt file. You should see the same message.
  3. Remove one value from a line. You should see a message such as "Line 2: expected 6 values but found 5."
  4. 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."
  5. 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.
  6. 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 Id value.

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.

  1. 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.
  2. Keep the size limit. The code checks ContentLength against 2 MB. ASP.NET also enforces its own limit, described in the troubleshooting section, and IIS has a separate one.
  3. 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.
  4. Treat every value as untrusted text. If you define your own BoundField columns, leave HtmlEncode at its default of true. 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.
  5. 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.
  6. 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.
  7. Don't put the upload inside an UpdatePanel. FileUpload doesn't work with asynchronous partial postbacks, so HasFile would always be false. If the page uses an UpdatePanel, register the Import button as a PostBackTrigger so 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.

XML
<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:

  1. Check that a file was chosen, its extension, and its size before reading.
  2. Read the upload stream directly. You don't need to save the file.
  3. Use a real CSV parser so quoted commas don't break your rows.
  4. Validate each record, parse numbers with CultureInfo.InvariantCulture, and report the row number when something is wrong.
  5. Web Forms runs on the .NET Framework only. Use ASP.NET Core for new projects.

Related reading

Official references

Codingvila provides articles and blogs on web and software development for beginners as well as free Academic projects for final year students in Asp.Net, MVC, C#, Vb.Net, SQL Server, Angular Js, Android, PHP, Java, Python, Desktop Software Application and etc.

avatar

Gridview1 is declared in CS.aspx.cs file. That is a aspx control written in the cs.aspx.cs file .

Make sure you have this code in your cs.aspx.cs file.

delete September 24, 2019 at 8:23:00 PM GMT+5:30
avatar

Hey how to display only particular columns from CSV file into grid view

delete May 27, 2021 at 10:25:00 PM GMT+5:30

If you have any questions, contact us on info.codingvila@gmail.com