A general-purpose framework that works with tabular data represented as delimiter-separated values, the main types are:
The low-level API flavor:
The TabularReader type provides forward-only, read-only access to tabular data fields as strings or typed values.
The TabularWriter type provides forward-only, write-only access to tabular data fields as strings or typed values.
The high-level API flavor:
The TabularReader<T> type provides forward-only, read-only access to tabular data records as typed plain objects.
The TabularWriter<T> type provides forward-only, write-only access to tabular data records as typed plain objects.
The minimal API flavor:
The TabularData type provides static methods for working with collections of tabular data records and inferring dialects.
The value converter abstraction provides an ability to work with tabular data fields as typed values, while the record handler abstraction provides an ability to work with tabular data records as typed plain objects by defining a complete read-write workflow. Although record handlers can be created manually, by default they are generated by the built-in source generator according to the metadata explicitly declared with attributes. Each API flavor requires an instance of the TabularDialect type that specifies how to read and write tabular data. Framework types that perform I/O operations provide synchronous and asynchronous API, including cancellation support.
How to Use
How to work with tabular data of a specific structure:
var dialect = new TabularDialect("\r\n", ',', '\"');
using (var writer = new TabularWriter<Book>(File.Create("books.csv"), dialect))
{
var book1 = new Book
{
Author = "Lewis Carroll",
Title = "Alice's Adventures in Wonderland",
Published = new(1865, 11, 09)
};
writer.WriteRecord(book1);
var book2 = new Book
{
Author = "H. G. Wells",
Title = "The Time Machine",
Published = new(1894, 03, 17)
};
writer.WriteRecord(book2);
}
using (var reader = new TabularReader<Book>(File.OpenRead("books.csv"), dialect))
{
while (reader.TryReadRecord())
{
var book = reader.CurrentRecord;
Console.WriteLine($"{book.Author} '{book.Title}' ({book.Published})");
}
}
[TabularRecord]
internal struct Book
{
[TabularFieldOrder(0)]
public string? Author;
[TabularFieldOrder(1)]
public string? Title;
[TabularFieldOrder(2)]
public DateOnly? Published;
}
var dialect = new TabularDialect("\r\n", ',', '\"');
using (var writer = new TabularWriter(File.Create("books.csv"), dialect))
{
writer.WriteString("Lewis Carroll");
writer.WriteString("Alice's Adventures in Wonderland");
writer.WriteDateOnly(new(1865, 11, 09));
writer.FinishRecord();
writer.WriteString("H. G. Wells");
writer.WriteString("The Time Machine");
writer.WriteDateOnly(new(1894, 03, 17));
writer.FinishRecord();
}
using (var reader = new TabularReader(File.OpenRead("books.csv"), dialect))
{
while (reader.TryPickRecord())
{
reader.TryReadField();
reader.TryGetString(out var field0);
reader.TryReadField();
reader.TryGetString(out var field1);
reader.TryReadField();
reader.TryGetDateOnly(out var field2);
Console.WriteLine($"{field0} '{field1}' ({field2})");
}
}
type internal Book =
struct
val mutable Author: string
val mutable Title: string
val mutable Published: Nullable<DateOnly>
end
let dialect = new TabularDialect("\r\n", ',', '\"')
using (new TabularWriter<Book>(File.Create "books.csv", dialect)) (fun writer ->
let book1 = new Book (
Author = "Lewis Carroll",
Title = "Alice's Adventures in Wonderland",
Published = new DateOnly(1865, 11, 09)
)
writer.WriteRecord &book1
let book2 = new Book (
Author = "H. G. Wells",
Title = "The Time Machine",
Published = new DateOnly(1894, 03, 17)
)
writer.WriteRecord &book2
)
using (new TabularReader<Book>(File.OpenRead "books.csv", dialect)) (fun reader ->
while reader.TryReadRecord () do
let book = reader.CurrentRecord
printfn $"{book.Author} '{book.Title}' ({book.Published})"
)
Note
Using the high-level API with F# in this example requires a custom record handler.
let dialect = new TabularDialect("\r\n", ',', '\"')
using (new TabularWriter(File.Create "books.csv", dialect)) (fun writer ->
writer.WriteString "Lewis Carroll"
writer.WriteString "Alice's Adventures in Wonderland"
writer.WriteDateOnly (new DateOnly(1865, 11, 09))
writer.FinishRecord ()
writer.WriteString "H. G. Wells"
writer.WriteString "The Time Machine"
writer.WriteDateOnly (new DateOnly(1894, 03, 17))
writer.FinishRecord ()
)
using (new TabularReader(File.OpenRead "books.csv", dialect)) (fun reader ->
while reader.TryPickRecord () do
let mutable field0 = Unchecked.defaultof<string>
let mutable field1 = Unchecked.defaultof<string>
let mutable field2 = Unchecked.defaultof<DateOnly>
reader.TryReadField () |> ignore
reader.TryGetString &field0 |> ignore
reader.TryReadField () |> ignore
reader.TryGetString &field1 |> ignore
reader.TryReadField () |> ignore
reader.TryGetDateOnly &field2 |> ignore
printfn $"{field0} '{field1}' ({field2})"
)
How to work with tabular data of a specific structure that has a header:
var dialect = new TabularDialect("\r\n", ',', '\"');
using (var writer = new TabularWriter<Book>(File.Create("books.csv"), dialect))
{
var book1 = new Book
{
Author = "Lewis Carroll",
Title = "Alice's Adventures in Wonderland",
Published = new(1865, 11, 09)
};
writer.WriteRecord(book1);
var book2 = new Book
{
Author = "H. G. Wells",
Title = "The Time Machine",
Published = new(1894, 03, 17)
};
writer.WriteRecord(book2);
}
using (var reader = new TabularReader<Book>(File.OpenRead("books.csv"), dialect))
{
while (reader.TryReadRecord())
{
var book = reader.CurrentRecord;
Console.WriteLine($"{book.Author} '{book.Title}' ({book.Published})");
}
}
[TabularRecord]
internal struct Book
{
[TabularFieldName("author")]
[TabularFieldOrder(0)]
public string? Author;
[TabularFieldName("title")]
[TabularFieldOrder(1)]
public string? Title;
[TabularFieldName("published")]
[TabularFieldOrder(2)]
public DateOnly? Published;
}
var dialect = new TabularDialect("\r\n", ',', '\"');
using (var writer = new TabularWriter(File.Create("books.csv"), dialect))
{
writer.WriteString("author");
writer.WriteString("title");
writer.WriteString("published");
writer.FinishRecord();
writer.WriteString("Lewis Carroll");
writer.WriteString("Alice's Adventures in Wonderland");
writer.WriteDateOnly(new(1865, 11, 09));
writer.FinishRecord();
writer.WriteString("H. G. Wells");
writer.WriteString("The Time Machine");
writer.WriteDateOnly(new(1894, 03, 17));
writer.FinishRecord();
}
using (var reader = new TabularReader(File.OpenRead("books.csv"), dialect))
{
while (reader.TryReadField())
{
}
while (reader.TryPickRecord())
{
reader.TryReadField();
reader.TryGetString(out var field0);
reader.TryReadField();
reader.TryGetString(out var field1);
reader.TryReadField();
reader.TryGetDateOnly(out var field2);
Console.WriteLine($"{field0} '{field1}' ({field2})");
}
}
type internal Book =
struct
val mutable Author: string
val mutable Title: string
val mutable Published: Nullable<DateOnly>
end
let dialect = new TabularDialect("\r\n", ',', '\"')
using (new TabularWriter<Book>(File.Create "books.csv", dialect)) (fun writer ->
let book1 = new Book (
Author = "Lewis Carroll",
Title = "Alice's Adventures in Wonderland",
Published = new DateOnly(1865, 11, 09)
)
writer.WriteRecord &book1
let book2 = new Book (
Author = "H. G. Wells",
Title = "The Time Machine",
Published = new DateOnly(1894, 03, 17)
)
writer.WriteRecord &book2
)
using (new TabularReader<Book>(File.OpenRead "books.csv", dialect)) (fun reader ->
while reader.TryReadRecord () do
let book = reader.CurrentRecord
printfn $"{book.Author} '{book.Title}' ({book.Published})"
)
Note
Using the high-level API with F# in this example requires a custom record handler.
let dialect = new TabularDialect("\r\n", ',', '\"')
using (new TabularWriter(File.Create "books.csv", dialect)) (fun writer ->
writer.WriteString "author"
writer.WriteString "title"
writer.WriteString "published"
writer.FinishRecord ()
writer.WriteString "Lewis Carroll"
writer.WriteString "Alice's Adventures in Wonderland"
writer.WriteDateOnly (new DateOnly(1865, 11, 09))
writer.FinishRecord ()
writer.WriteString "H. G. Wells"
writer.WriteString "The Time Machine"
writer.WriteDateOnly (new DateOnly(1894, 03, 17))
writer.FinishRecord ()
)
using (new TabularReader(File.OpenRead "books.csv", dialect)) (fun reader ->
while reader.TryReadField () do
()
while reader.TryPickRecord () do
let mutable field0 = Unchecked.defaultof<string>
let mutable field1 = Unchecked.defaultof<string>
let mutable field2 = Unchecked.defaultof<DateOnly>
reader.TryReadField () |> ignore
reader.TryGetString &field0 |> ignore
reader.TryReadField () |> ignore
reader.TryGetString &field1 |> ignore
reader.TryReadField () |> ignore
reader.TryGetDateOnly &field2 |> ignore
printfn $"{field0} '{field1}' ({field2})"
)
How to work with tabular data of a specific structure using the minimal API:
var dialect = new TabularDialect("\r\n", ',', '\"');
var books = new Book[]
{
new()
{
Author = "Lewis Carroll",
Title = "Alice's Adventures in Wonderland",
Published = new(1865, 11, 09)
},
new()
{
Author = "H. G. Wells",
Title = "The Time Machine",
Published = new(1894, 03, 17)
}
};
TabularData.WriteRecords(File.Create("books.csv"), dialect, books);
books = TabularData.ReadRecords<Book>(File.OpenRead("books.csv"), dialect);
foreach (var book in books)
{
Console.WriteLine($"{book.Author} '{book.Title}' ({book.Published})");
}
[TabularRecord]
internal struct Book
{
[TabularFieldOrder(0)]
public string? Author;
[TabularFieldOrder(1)]
public string? Title;
[TabularFieldOrder(2)]
public DateOnly? Published;
}
Note
The minimal API is an extension for the high-level API.
type internal Book =
struct
val mutable Author: string
val mutable Title: string
val mutable Published: Nullable<DateOnly>
end
let dialect = new TabularDialect("\r\n", ',', '\"')
let mutable private books = [|
new Book (
Author = "Lewis Carroll",
Title = "Alice's Adventures in Wonderland",
Published = new DateOnly(1865, 11, 09)
)
new Book (
Author = "H. G. Wells",
Title = "The Time Machine",
Published = new DateOnly(1894, 03, 17)
)
|]
TabularData.WriteRecords(File.Create "books.csv", dialect, books)
books <- TabularData.ReadRecords<Book>(File.OpenRead "books.csv", dialect)
for book in books do
printfn $"{book.Author} '{book.Title}' ({book.Published})"
Note
Using the high-level API with F# in this example requires a custom record handler.
Note
The minimal API is an extension for the high-level API.