A Field Guide to .NET Data Formats
In Part 1, we made the case that every data format grows its own language because the shape of the data dictates what kind of navigation makes sense. This part is the reference you actually bookmark: a walk through the formats a .NET developer runs into, the native language each one speaks, and a small idiomatic snippet for each so you can see the shape of the code, not just the theory.
Think of this as a field guide, not a tutorial — enough to recognize each format's dialect and know where to look next.
CSV and Delimited Text
Native language: none, really — just string parsing conventions, though CsvHelper has become close to a de facto DSL for mapping rows to types.
CSV is the oldest, least structured format on this list. There's no query language because there's barely a schema — just rows and a delimiter. In modern .NET, CsvHelper is the standard library, handling quoting, escaping, and type mapping that hand-rolled String.Split code gets wrong more often than people expect.
using CsvHelper;
using System.Globalization;
using var reader = new
StreamReader("orders.csv");
using var csv = new CsvReader
(reader, CultureInfo.InvariantCulture);
var orders = csv.GetRecords()
.Where(o => o.Total > 100)
.ToList();
Notice that once the CSV is parsed into Order objects, the "querying" is just LINQ to Objects. CSV's native language is really just parsing — the querying happens after conversion into another format.
Reach for this when: you're dealing with exports, imports, or logs where the data is naturally tabular and there's no need for relationships between records.
RDBMS: The Original Query Language
Native language: SQL.
Relational databases are the format most .NET developers meet first, and SQL is the most mature and expressive language in this whole list. .NET has three common ways to speak it, at increasing levels of abstraction:
// Raw ADO.NET — closest to the metal
using var connection = new
SqlConnection(connectionString);
using var command = new SqlCommand(
"SELECT * FROM Orders
WHERE Total > @minTotal",
connection);
command.Parameters.AddWithValue
("@minTotal", 100);
connection.Open();
using var reader = command.ExecuteReader();
// Dapper — a thin, popular micro-ORM
var orders = connection.Query<Order>(
"SELECT * FROM Orders WHERE Total
> @minTotal", new { minTotal = 100 });
// Entity Framework Core — LINQ translated to SQL
var orders = dbContext.Orders
.Where(o => o.Total > 100)
.ToList();
The EF Core example is worth sitting with: you write LINQ, but under the hood, it's compiled into SQL and sent to the database. This is LINQ's provider model in action — the same syntax, translated into the format's native tongue. It's convenient, but it's also the first place developers hit the "leaky abstraction" problem this series will return to in Part 3 — certain LINQ expressions don't translate cleanly, and the generated SQL isn't always what you'd write by hand.
Reach for this when: your data has strong relationships, needs transactional integrity, or benefits from set-based operations like joins and aggregations.
XML: The Tree Format
Native languages: XPath, XQuery, and in .NET, LINQ to XML.
XML data is a tree, so its languages are built around traversal — descending into children, matching by path, filtering by attribute.
using System.Xml.Linq;
XDocument doc = XDocument.Load
("catalog.xml");
// LINQ to XML — tree traversal expressed as LINQ
var fictionBooks = doc.Descendants("book")
.Where(b => (string)b.Attribute
("category") == "fiction")
.Select(b => (string)b.
Element("title"));
// Equivalent XPath, for comparison
var xpathResult = doc.XPathSelectElements(
"//book[@category='fiction']/title");
Both snippets do the same job, but they read differently — LINQ to XML feels like querying a collection, while XPath feels like giving directions through a tree. That's not an accident: XPath was purpose-built for tree navigation, and LINQ to XML is a later, more C#-idiomatic wrapper around the same underlying traversal.
Reach for this when: you're working with configuration files, SOAP services, document interchange formats (like Office Open XML), or anything with a strict, validatable schema (XSD).
JSON: The Web's Lingua Franca
Native language: no single formal standard, but a mix of strongly-typed deserialization, dynamic access, and JSONPath for ad hoc queries.
JSON overtook XML for API payloads because it's lighter and maps naturally onto JavaScript objects. .NET's relationship with JSON has gone through Newtonsoft.Json (Json.NET) to the built-in System.Text.Json.
using System.Text.Json;
string json = await httpClient.GetStringAsync
("https://api.example.com/orders");
// Strongly-typed — the common case
var orders = JsonSerializer.
Deserialize<List<Order>>(json);
var bigOrders = orders.Where
(o => o.Total > 100);
// Loosely-typed — when the shape isn't known ahead of time
using JsonDocument doc =
JsonDocument.Parse(json);
JsonElement root = doc.RootElement;
foreach (var order in
root.EnumerateArray())
{
if (order.GetProperty("total").
GetDouble() > 100)
{
Console.WriteLine
(order.GetProperty("id").GetString());
}
}
Once JSON is deserialized into objects, the "language" you use is LINQ to Objects again — the same pattern we saw with CSV. JSON's distinguishing trait is that it straddles two worlds: rigid, typed deserialization when you control both ends, and loose, path-based navigation (JsonDocument, or JSONPath libraries) when you're dealing with a document whose shape you don't fully control.
Reach for this when: you're building or consuming HTTP APIs, or storing semi-structured data where a rigid schema is more trouble than it's worth.
Document Databases: JSON's Native Habitat
Native language: SQL-like dialects purpose-built for JSON documents — Cosmos DB's SQL API, MongoDB's query language.
Document databases store JSON-shaped documents but query them with something that looks like SQL — a deliberate design choice to make the language approachable, even though the underlying model isn't relational.
using Microsoft.Azure.Cosmos;
Container container = client.GetContainer
("SalesDb", "Orders");
var query = new QueryDefinition(
"SELECT * FROM o
WHERE o.total > @minTotal")
.WithParameter("@minTotal", 100);
using FeedIterator<Order> iterator =
container.GetItemQueryIterator<Order>(query);
while (iterator.HasMoreResults)
{
FeedResponse<Order> response =
await iterator.ReadNextAsync();
foreach (var order in response) { /* ... */ }
}
This is a good illustration of the format-language relationship getting blurry on purpose: Cosmos DB's query language borrows SQL's syntax for familiarity, but it's operating over JSON documents, not relational rows — no joins across containers, different indexing behavior, different cost model. The vocabulary is borrowed; the grammar underneath is not the same.
Reach for this when: your data is naturally document-shaped, your schema evolves frequently, or you need horizontal scale that a traditional RDBMS makes expensive.
Graph Data: Relationships as First-Class Citizens
Native languages: GraphQL (for API querying), Cypher/Gremlin (for graph databases).
Graphs are the format where the relationships between records matter as much as the records themselves — social networks, recommendation systems, fraud detection. In .NET, GraphQL support usually comes via a library like HotChocolate, exposing a schema that clients query with GraphQL's own syntax:
query {
order(id: "1001") {
id
total
customer {
name
previousOrders(last: 5) {
total
}
}
}
}
Note that GraphQL, despite its name, is more an API query language than a true graph-traversal language — it lets a client ask for exactly the nested shape of data it wants, which happens to be a natural fit for graph-like relationships but isn't the same as querying a graph database directly. True graph databases like Neo4j use traversal languages like Cypher (MATCH (o:Order)-[:PLACED_BY]->(c:Customer) WHERE o.total > 100 RETURN c), which .NET applications typically call through a driver rather than write in C#-native syntax.
Reach for this when: the relationships in your data are the point — not just an attribute to look up, but the thing you're actually trying to traverse and analyze.
Binary and Serialized Formats
Native language: none, by design.
Formats like Protocol Buffers (protobuf) and MessagePack are the outliers on this list — they're optimized for compactness and speed, not human readability or ad hoc querying. There's no query language because you're not meant to query the binary form directly; you deserialize it into objects first, then query those objects like anything else.
Binary formats are interesting because they deliberately avoid becoming a human-readable representation. Their goal isn't to be explored with queries but to move data across process or network boundaries as efficiently as possible. Once deserialized, they immediately become ordinary CLR objects again.
// Protobuf — schema-defined, then generated C# classes
Order order = Order.Parser.ParseFrom(binaryData);
// From here, it's just an object again
if (order.Total > 100) { /* ... */ }
Reach for this when: performance and payload size matter more than readability — internal service-to-service communication, high-throughput event streams, caching layers.
Objects in Memory: The Common Destination
Native language: LINQ to Objects.
Here's the pattern that's been quietly running through nearly every example above: no matter what format the data started in — CSV rows, JSON documents, database rows, deserialized protobuf — once it's loaded into C#, it becomes objects, and the language you use to query it is the same: LINQ to Objects.
var bigRecentOrders = orders
.Where(o => o.Total >
100 && o.Date > DateTime.Now.AddDays(-30))
.OrderByDescending(o => o.Total)
.Select(o => new { o.Id, o.Total });
This is worth calling out explicitly, because it reframes the whole field guide: most of these formats aren't really competing languages you use instead of LINQ — they're the languages you use to get data into a form where LINQ can take over. RDBMS and XML are really the only two formats on this list with a genuinely independent, mature query language that competes with LINQ on its own turf, which is exactly why Part 3 will focus on those cases when it looks at where LINQ's abstraction holds and where it doesn't.
The Field Guide at a Glance
| Format |
Native Language |
Common .NET Tooling |
Query Style |
| CSV / delimited |
None (parsing conventions) |
CsvHelper |
Row-by-row, then LINQ |
| RDBMS |
SQL |
ADO.NET, Dapper, EF Core |
Set-based (filter, join, aggregate) |
| XML |
XPath, XQuery |
LINQ to XML, XDocument |
Tree traversal |
| JSON |
None formal; JSONPath for ad hoc |
System.Text.Json, Newtonsoft.Json |
Typed deserialization or path-based |
| Document DB |
SQL-like dialects |
Cosmos SDK, MongoDB driver |
SQL-flavored, document-scoped |
| Graph |
GraphQL, Cypher/Gremlin |
HotChocolate, Neo4j driver |
Relationship traversal |
| Binary/serialized |
None (not queryable directly) |
protobuf-net, MessagePack |
Deserialize, then LINQ |
| Objects in memory |
LINQ to Objects |
Built into .NET |
Functional (filter, map, reduce) |
Line them up like this and the pattern from Part 1 becomes visible again: the more structured and relationship-heavy the format (RDBMS, XML, graphs), the more mature and independent its native language is. The less structured the format (CSV, binary), the more the "language" is really just a parsing step on the way to becoming objects — at which point LINQ takes over.
What's Next
We now have both a mental model (Part 1) and a working reference (this part) for the format-language landscape in .NET. In Part 3, we'll return to LINQ specifically and ask how well it holds up as a unifying layer across all of this — where it succeeds, where the abstraction leaks, and how a newer format, vector embeddings and similarity search, is already forcing the next iteration of this same pattern.
That’s all for now. May your intention be clear and your mind be still. With this quiet wish, I rest my pen and return to the silence.