5. LadybugDB from C#
Sections
Reading settings
Natural voice recorded for this course, the same in every browser.
No voice for this language in this browser.
More voices
Code and English terms
Off: the page’s voice reads everything, with no pause between two voices.
No voice for this language in this browser.
More voices
More voices: Microsoft Edge has natural online voices, and Windows (Settings › Time & language › Speech), macOS and Android install others.
From C#, LadybugDB runs in your process, as a native library called through the LadybugDB NuGet package. The package isn’t an ADO.NET provider: no DbConnection, no DbDataReader, and so no Dapper. Its classes are close to them, though:
| ADO.NET | LadybugDB 0.19.1 |
|---|---|
| a connection string | new Database(path, config): the database itself, in your process |
DbConnection |
new Connection(database): several connections can share one Database |
DbCommand.ExecuteReader() |
connection.Query(cypher) returns a QueryResult |
DbDataReader.Read() and GetInt64(i) |
result.Rows(): an IEnumerable<object?[]> |
DbParameter |
connection.Execute(cypher, dictionary) or Prepare(cypher) then Bind(name, value) |
DbException |
LadybugException and LadybugQueryException |
The program of this lesson is csharp/ProjectGraph/Program.cs, run from code/ladybugdb and compared with csharp/expected.txt on the three operating systems. The package has no page in the LadybugDB documentation: its reference is the README of ladybug-dotnet and its source code.
A new graph: the projects of GuitarAlchemist/ga
Section titled “A new graph: the projects of GuitarAlchemist/ga”Lessons 1 to 4 queried this site. Lessons 5 and 6 query a .NET solution: GuitarAlchemist/ga, a music theory application with 111 C# and F# projects. data/ga/extract.py reads its project files at commit a26a7893 and writes three CSV files to data/ga:
| Table | Kind | From |
|---|---|---|
Project(path, name, language, sdk, frameworks, in_solution) |
node | projects.csv: each .csproj and .fsproj, and whether AllProjects.slnx lists it |
Package(name) |
node | the distinct packages of package_refs.csv |
REFERENCES |
relationship, Project to Project |
project_refs.csv: the <ProjectReference> items |
USES(version) |
relationship, Project to Package |
package_refs.csv: the <PackageReference> items, with their version |
A dependency graph is what dotnet build walks, and what Visual Studio’s dependency diagrams draw; here it’s a graph you can query.
version is the version written in each project file. The Directory.Build.props of ga changes some of them for every project, with <PackageReference Update="…" Version="…"/> items: Microsoft.Extensions.Options, System.Numerics.Tensors and twelve others. The extraction doesn’t apply them; the queries of this lesson use packages that file doesn’t touch.
The project
Section titled “The project”<ItemGroup> <PackageReference Include="LadybugDB" Version="0.19.1" /> <PackageReference Include="LadybugDB.Native" Version="0.19.1" /></ItemGroup>LadybugDB is the managed code, calling the C API of the engine with P/Invoke. The engine itself is in a native package per runtime identifier: LadybugDB.Native.win-x64, linux-x64, linux-arm64, osx-x64 and osx-arm64. LadybugDB.Native references all five:
| Size | |
|---|---|
| the five native packages in the NuGet cache | 25 MB (win-x64) to 38 MB (linux-x64) each |
bin/Release/net10.0 after dotnet build |
131 MB, of which 19 to 28 MB per runtime |
dotnet publish -r win-x64 |
20 MB |
An application for a single platform can reference LadybugDB.Native.win-x64 alone instead of the meta-package.
The version is behind the CLI. 0.19.1 is the latest version on NuGet, and the README says the first three numbers of the package version are the engine’s: this program runs the 0.19.1 engine, the CLI of lessons 1 to 4 is 0.20.4. The program prints both versions it knows (lines 10-14):
Console.WriteLine($"LadybugDB {LadybugVersion.Version}, storage version {LadybugVersion.StorageVersion}");// An empty path: an in-memory database, like the CLI started without a file nameusing var database = new Database("");using var connection = new Connection(database);LadybugDB 0.19.1, storage version 43The storage version is the format of database files. It matters when the program and the CLI open the same file, below.
Loading: a reference to a project that isn’t there
Section titled “Loading: a reference to a project that isn’t there”The schema and the COPY statements are those of lesson 2, sent one by one with connection.Query (lines 16-32):
Execute("COPY Project FROM 'data/ga/projects.csv' (HEADER = true)");try{ Execute("COPY REFERENCES FROM 'data/ga/project_refs.csv' (HEADER = true)");}catch (LadybugQueryException e){ Console.WriteLine($"{e.GetType().Name}: {e.Message}"); Execute("COPY REFERENCES FROM 'data/ga/project_refs.csv' (HEADER = true, IGNORE_ERRORS = true)");}111 tuples have been copied to the Project table.LadybugQueryException: Copy exception: Unable to find primary key value Experiments/React/reactapp1.client/reactapp1.client.esproj.265 tuples have been copied to the REFERENCES table.1 warnings encountered during copy. Use 'CALL show_warnings() RETURN *' to view the actual warnings. Query ID: 6Execute is a helper of the program that prints the first column of every row. ReactApp1.Server.csproj references reactapp1.client.esproj, a JavaScript project that the extraction didn’t keep: the error of lesson 2, now a .NET exception. The CLI printed the error and went on; in C#, the failed COPY throws and nothing is loaded until the retry with IGNORE_ERRORS.
using (var result = connection.Query(""" MATCH (p:Project)-[:REFERENCES]->(core:Project) RETURN core.name, count(*) AS referenced_by ORDER BY referenced_by DESC, core.name LIMIT 3 """)){ Console.WriteLine($"{string.Join(", ", result.ColumnNames)}: {result.RowCount} rows"); foreach (var row in result.Rows()) { Console.WriteLine($"{row[0],-20} {row[1],2} ({row[1]?.GetType()})"); } Console.WriteLine($"a second foreach: {result.Rows().Count()} rows");}core.name, referenced_by: 3 rowsGA.Domain.Core 57 (System.Int64)GA.Domain.Services 48 (System.Int64)GA.Core 32 (System.Int64)a second foreach: 0 rowsRowCountis known before the first row: the engine has computed the whole result.- Each row is an
object?[], andINT64arrives boxed aslong. Rows()can be enumerated only once, like aDbDataReader, but it doesn’t say so: the secondforeachfinds no row, and no exception.Rows()is an iterator over a cursor of the native result (QueryResult.cs, lines 117-133). Call.ToList()when you need the rows twice.QueryResultisIDisposable: it holds native memory untilDispose.
57 of the 111 projects reference GA.Domain.Core.
Parameters
Section titled “Parameters”Two ways, with a dictionary or with a prepared statement (lines 51-67):
using (var result = connection.Execute( "MATCH (p:Project {name: $name})-[:REFERENCES*1..10]->(d:Project) RETURN count(DISTINCT d) AS dependencies", new Dictionary<string, object?> { ["name"] = "GaApi" })){ Console.WriteLine($"GaApi depends on {result.Rows().Single()[0]} projects");}using (var statement = connection.Prepare(""" MATCH (p:Project {name: $name})-[:REFERENCES*1..10]->(d:Project) RETURN count(DISTINCT d) AS dependencies """)){ foreach (var name in new[] { "GaCli", "GaMcpServer", "GA.Domain.Core" }) { using var result = statement.Bind("name", name).Execute(); Console.WriteLine($"{name} depends on {result.Rows().Single()[0]} projects"); }}GaApi depends on 20 projectsGaCli depends on 7 projectsGaMcpServer depends on 13 projectsGA.Domain.Core depends on 2 projects$name in Cypher, "name" without the $ in C#. Execute with a dictionary prepares the statement on each call; Prepare once, then Bind and Execute for each value, saves that work, like DbCommand.Prepare(). Bind returns the statement, so the calls chain. The web API, GaApi, depends on 20 projects directly or not: the variable-length pattern of lesson 3 finds them all.
What the binding accepts, and what it reports (lines 68-83):
int 2: 42 projects reference more than 2 projectsstring "2": 42 projects reference more than 2 projectsdecimal 2m: NotSupportedException: Cannot bind a parameter of type System.Decimal.LadybugQueryException: Parameter name not found.LadybugQueryException: Binder exception: Cannot find parameter file. This should not happen.- The query compares
COUNT { … } > $n. Anintworks, and so does the string"2": the engine casts it to a number. SQL Server does the same with annvarcharparameter compared to anintcolumn. - A
decimalfails in the binding, before the engine sees it (PreparedStatement.cs, line 198): pass adouble, or a string cast in Cypher. - A dictionary with
nominstead ofnamefails withParameter name not found., without the name. - A parameter can’t be a file name:
COPY Package FROM $filefails at bind time, with a message that says it should not happen. Build the file name into the statement, from a trusted value.
Nodes, relationships and paths
Section titled “Nodes, relationships and paths”A query can return a whole node or a whole path (lines 85-104):
using (var result = connection.Query(""" MATCH path = (app:Project {name: 'GaApi'})-[:REFERENCES* ALL SHORTEST 1..10]->(assets:Project {name: 'GA.Business.Assets'}) RETURN app, path """)){ var rows = result.Rows().ToList(); var app = (Node)rows[0][0]!; // The internal ID (table:offset) depends on the load, which differs between runs and machines: print its type only Console.WriteLine($"{app.Label} ({app.Id.GetType().Name}): {string.Join(", ", app.Properties.Select(p => $"{p.Key}={p.Value}"))}"); // Several shortest paths, in no particular order: sort them before printing foreach (var path in rows.Select(row => (RecursiveRel)row[1]!) .Select(path => $"{path.Rels.Count} relationships: {string.Join(" -> ", path.Nodes.Select(n => n.Properties["name"]))}") .Order()) { Console.WriteLine(path); } var first = ((RecursiveRel)rows[0][1]!).Rels[0]; Console.WriteLine($"first relationship: {first.Label}, starts at GaApi: {first.Source == app.Id}");}Project (InternalId): path=Apps/ga-server/GaApi/GaApi.csproj, name=GaApi, language=C#, sdk=Microsoft.NET.Sdk.Web, frameworks=net10.0, in_solution=True3 relationships: GaApi -> GA.Business.AI -> GA.Data.MongoDB -> GA.Business.Assets3 relationships: GaApi -> GA.Business.ML -> GA.Data.MongoDB -> GA.Business.Assetsfirst relationship: REFERENCES, starts at GaApi: TrueThe binding maps graph values to records (GraphTypes.cs): Node(Id, Label, Properties), Rel(Id, Source, Destination, Label, Properties), and RecursiveRel(Nodes, Rels) for a path. Properties is a dictionary; Source and Destination are the InternalId of the nodes, not the nodes. The Nodes of a named path include both ends.
The first version of this section printed app.Id and used SHORTEST, and the CI failed on the three runners:
- the ID of
GaApiwas0:11on my machine and on the Windows runner,0:47on Linux and macOS, with the same CSV file. An internal ID is the table and the offset where the node was stored: use it within a result, to connect aRelto its nodes, never as a key you keep; - there are two shortest paths, through
GA.Business.AIorGA.Business.ML, andSHORTESTreturned one on my machine and the other on the runners.ALL SHORTESTreturns both, sorted by the program.
Lists and structs
Section titled “Lists and structs”Which packages are used in several versions? (lines 106-121)
using (var result = connection.Query(""" MATCH (p:Project)-[u:USES]->(k:Package) WITH k, count(*) AS projects, collect(DISTINCT u.version) AS versions WHERE size(versions) > 1 RETURN k.name, versions, {projects: projects, versions: size(versions)} AS usage ORDER BY size(versions) DESC, k.name LIMIT 3 """)){ foreach (var row in result.Rows()) { var versions = (object?[])row[1]!; var usage = (Dictionary<string, object?>)row[2]!; Console.WriteLine($"{row[0]}: {string.Join(", ", versions.Order())} ({usage["projects"]} projects)"); }}Microsoft.Extensions.Hosting: 10.0.0, 10.0.5, 9.0.0, 9.0.10, 9.0.4 (12 projects)MongoDB.Driver: 2.29.0, 2.30.0, 3.2.0, 3.2.1, 3.5.0 (14 projects)Microsoft.Extensions.DependencyInjection: 10.0.0, 10.0.2, 9.0.0, 9.0.10 (12 projects)A LIST arrives as object?[], not string[]: cast each element. A STRUCT arrives as a Dictionary<string, object?>. collect gives no order, so the program sorts; Order() on strings puts 10.0.0 before 9.0.0.
MongoDB.Driver is used in five versions, across two major versions. Exercise 2 looks at what that means for the projects that reference each other.
The aggregate bug, in 0.19.1 too
Section titled “The aggregate bug, in 0.19.1 too”count(*) comes before collect(DISTINCT …) in the query above, and not by chance (lines 123-130):
MATCH (p:Project)-[u:USES]->(k:Package {name: 'MongoDB.Driver'})RETURN k.name, collect(DISTINCT u.version) AS versions, count(*) AS projectsDISTINCT first: MongoDB.Driver, 5 versions, 0 projects0 projects instead of 14. It’s the bug of the journal, in a more general form: with a grouping key, an aggregate that follows a DISTINCT aggregate in the same projection is wrong, NULL for sum and 0 for count. It is in the 0.19.1 engine of the package as in the 0.20.4 CLI. The workaround is the same: put the DISTINCT aggregates last.
Rows as records
Section titled “Rows as records”No Dapper, but a ten-line helper maps rows to a record (lines 138-145 and 249-256):
IEnumerable<T> Query<T>(string cypher, Func<object?[], T> map){ using var result = connection.Query(cypher); foreach (var row in result.Rows()) { yield return map(row); }}foreach (var project in Query( "MATCH (p:Project) WHERE NOT p.in_solution RETURN p.name, p.path, p.language ORDER BY p.path LIMIT 3", row => new Project((string)row[0]!, (string)row[1]!, (string)row[2]!))){ Console.WriteLine(project);}Project { Name = FloorManager, Path = Apps/FloorManager/FloorManager.csproj, Language = C# }Project { Name = GaChatbot, Path = Apps/GaChatbot/GaChatbot.csproj, Language = C# }Project { Name = InteractiveTutorial, Path = Apps/InteractiveTutorial/InteractiveTutorial.csproj, Language = C# }38 projects are not in AllProjects.slnxThe using inside the iterator disposes of the result when the foreach ends, even when it ends early with break or Single(). 38 of the 111 projects aren’t in AllProjects.slnx: a build of the solution doesn’t compile them.
Dates and times
Section titled “Dates and times”Lines 147-167 send a DateTimeOffset and a DateTime for 9:30 in New York, and compare with a literal:
using (var result = connection.Execute( "RETURN $offset AS offset, $local AS local, timestamp('2026-09-14T09:30:00-04:00') AS literal", new Dictionary<string, object?> { ["offset"] = new DateTimeOffset(2026, 9, 14, 9, 30, 0, TimeSpan.FromHours(-4)), ["local"] = new DateTime(2026, 9, 14, 9, 30, 0), }))offset DateTimeOffset 2026-09-14 13:30:00 +00:00local DateTime 2026-09-14 09:30:00, Kind = Utcliteral DateTime 2026-09-14 09:30:00, Kind = Utc- A
DateTimeOffsetbecomes aTIMESTAMP_TZ, stored in UTC: it comes back as 13:30 at+00:00, the same instant. - A
DateTimebecomes aTIMESTAMP. ItsKindisUnspecifiedhere, and the binding takes it as UTC (PreparedStatement.cs, lines 168-171); aLocalone is converted withToUniversalTime(). It comes back withKind = Utc. - The literal is wrong.
timestamp('…-04:00')returns 9:30: the offset is dropped without converting, where theCOPYof lesson 4 converted the same text to UTC. The 0.20.4 CLI does the same, andCAST(… AS TIMESTAMP)too;CAST(… AS TIMESTAMP_TZ)gives 13:30. A sixth bug for the journal.
Errors
Section titled “Errors”LadybugQueryException: Binder exception: Table Projet does not exist.LadybugQueryException: Binder exception: Cannot find property nam for p.LadybugQueryException: Runtime exception: Found duplicated primary key value Common/GA.Core/GA.Core.csproj, which violates the uniqueness constraint of the primary key column.two statements, one result: COUNT_STAR() = 111packages created by two statements: Test.A, Test.BEvery query error is a LadybugQueryException, with the kind at the start of the message: Binder exception for what the engine rejects before running, Runtime exception for what fails while running. There is no error code to test, unlike SqlException.Number.
Several statements in one Query return the first result only, and run them all: MATCH (p:Project) RETURN count(*); MATCH (k:Package) RETURN count(*) gives the project count, and the package count is lost; the two CREATE statements both created their node. The C API has lbug_query_result_has_next_query_result for the next results (lbug.h, line 771), and lbug_connection_interrupt and lbug_connection_set_query_timeout to stop a long query (lines 465 and 472); the binding 0.19.1 exposes none of them. Send one statement per call.
Sharing a file with the CLI
Section titled “Sharing a file with the CLI”new Database("out/net/ga.lbdb") creates or opens a database file. check.sh, lines 14-24, creates one from C#, queries it with the CLI, then opens it from C# again, twice:
created out/net/ga.lbdb with LadybugDB 0.19.1-- the CLI, read-onlyp.nameGA.Coreopened out/net/ga.lbdb: GA.Core-- the CLI, read-writep.nameGA.CoreLadybugException: Failed to open Ladybug database at 'out/net/ga.lbdb'.The 0.20.4 CLI reads the file written by 0.19.1 in both modes. But opened read-write, it converts the file to its own storage version, 47 instead of 43 (storage_version_info.h, lines 46-49), without a message: after that, the C# program can’t open it, and the exception doesn’t say why. With --read_only, the file stays as it was. From C#, new SystemConfig { ReadOnly = true } does the same.
As long as the package lags behind the CLI, open the application’s files with lbug --read_only. Several processes can open a file read-only at the same time; only one can open it read-write (concurrency).
Threads and connections
Section titled “Threads and connections”timings mode runs the same query, the 7,793 walks of the project graph up to 10 relationships, three ways (lines 221-236). Times in milliseconds, from the CI run of this lesson and from my machine; they vary from run to run, so CI prints them without comparing them:
| one query | 4 queries, 1 connection, 4 threads | 4 queries, 4 connections, 4 threads | |
|---|---|---|---|
| my machine (Windows, 24 threads) | 12 | 49 | 25 |
| Linux runner | 9 | 49 | 20 |
| macOS runner | 10 | 53 | 27 |
| Windows runner | 17 | 76 | 42 |
A Connection can be used from several threads, but it holds a lock for each call (Connection.cs, line 40): four queries on one connection take four times as long as one. One connection per thread, on the same Database, runs them side by side. In an ASP.NET Core application, that means one Database for the process, a singleton, and a Connection per request or per unit of work.
Key takeaways
Section titled “Key takeaways”- The
LadybugDBpackage isn’t ADO.NET:Database,Connection,QueryorExecute, andRows()asobject?[]. No Dapper; a small iterator maps rows to records. LadybugDB.Nativecarries the engine for five runtimes; a singleLadybugDB.Native.<rid>ordotnet publish -rkeeps one. The package is on engine 0.19.1, behind the 0.20.4 CLI.Rows()can be enumerated once: the second time it’s empty, silently.- Parameters: a dictionary, or
PrepareandBindto reuse the statement. Nodecimal, no file names. - Graph values come back as
Node,RelandRecursiveRelrecords. Internal IDs and the path chosen bySHORTESTcan differ from one machine to another. - A
DISTINCTaggregate before another aggregate breaks it in 0.19.1 too;timestamp()drops the offset of its argument. Querywith several statements returns the first result and runs them all.- A read-write open by a newer CLI upgrades the file beyond what the package can read: use
--read_only. - One
Databaseper process, oneConnectionper thread.
Exercises
Section titled “Exercises”The solutions are at the end of Program.cs, and their output is in expected.txt: CI checks them with the rest.
- Write
List<string> Dependents(PreparedStatement statement, string name), which returns the names of the projects that depend on a project, directly or not. Prepare the statement once and call the method forGA.Business.Assets,GA.Data.MongoDBandGaApi. How many dependents doesGA.Business.Assetshave?
Solution
Lines 180-192 and 258-262:
using (var dependents = connection.Prepare(""" MATCH (d:Project)-[:REFERENCES*1..10]->(:Project {name: $name}) RETURN DISTINCT d.name ORDER BY d.name """)){ foreach (var name in new[] { "GA.Business.Assets", "GA.Data.MongoDB", "GaApi" }) { var names = Dependents(dependents, name); Console.WriteLine($"{name}: {names.Count} dependents{(names.Count > 0 ? $", from {names[0]} to {names[^1]}" : "")}"); }}static List<string> Dependents(PreparedStatement statement, string name){ using var result = statement.Bind("name", name).Execute(); return result.Rows().Select(row => (string)row[0]!).ToList();}GA.Business.Assets: 31 dependents, from AllProjects.AppHost to VectorSearchBenchmarkGA.Data.MongoDB: 29 dependents, from AllProjects.AppHost to VectorSearchBenchmarkGaApi: 4 dependents, from AllProjects.AppHost to VectorSearchBenchmarkThe arrow is reversed compared with section 4: (d)-[…]->(:Project {name: $name}). ToList() reads the rows before the result is disposed, and returns a list the caller can enumerate as often as it wants.
31 names, but 32 projects: count(DISTINCT d) gives 32. Two projects are called GaApi.Tests, in Tests/Apps/GaApi.Tests and in Tests/GaApi.Tests, and DISTINCT d.name merges them. name isn’t the key: {name: $name} can match several nodes, which path, the primary key, can’t.
- A project that uses one major version of a package and references, directly or not, a project that uses another major version gets a single version at build time: NuGet picks one for the whole dependency graph of the project. Find these conflicts for
MongoDB.DriverandMicrosoft.ML.Tokenizers, with a prepared statement that takes the package name.
Solution
using (var conflicts = connection.Prepare(""" MATCH (a:Project)-[ua:USES]->(k:Package {name: $package}), (a)-[:REFERENCES*1..10]->(b:Project)-[ub:USES]->(k) WHERE split_part(ua.version, '.', 1) <> split_part(ub.version, '.', 1) RETURN DISTINCT a.name, ua.version, b.name, ub.version ORDER BY a.name, b.name """)){ foreach (var package in new[] { "MongoDB.Driver", "Microsoft.ML.Tokenizers" }) { using var result = conflicts.Bind("package", package).Execute(); foreach (var row in result.Rows()) { Console.WriteLine($"{package}: {row[0]} {row[1]}, but {row[2]} {row[3]}"); } }}MongoDB.Driver: GA.Analytics.Service 3.2.0, but GA.Data.MongoDB 2.30.0MongoDB.Driver: GA.BSP.Service 3.2.0, but GA.Data.MongoDB 2.30.0MongoDB.Driver: GA.DocumentProcessing.Service 3.2.0, but GA.Data.MongoDB 2.30.0MongoDB.Driver: GaApi 3.5.0, but GA.Data.MongoDB 2.30.0Microsoft.ML.Tokenizers: GaApi 1.0.2, but GA.Business.ML 2.0.0Microsoft.ML.Tokenizers: GaApi 1.0.2, but GA.Domain.Services 2.0.0The pattern names k twice, so both USES relationships go to the same package. split_part(…, 1) takes the major version.
NuGet’s rule is that a direct reference wins. GaApi gets MongoDB.Driver 3.5.0, and GA.Data.MongoDB, compiled against 2.30.0, runs with 3.5.0 in it: a new major version may have removed what the older code calls. For Microsoft.ML.Tokenizers, the direct reference is the lower one: GaApi gets 1.0.2 although two of its dependencies ask for at least 2.0.0. NuGet reports that downgrade as NU1605, an error by default in SDK projects, but ga’s Directory.Build.props suppresses NU1605 for the whole solution. Whether these projects fail at run time, on a missing method for instance, is to verify: I haven’t built or run ga for this lesson.
(long)row[0]!reads a count. Why does(int)row[0]!throw, and what are two ways to get anint?
Solution
using (var result = connection.Query("MATCH (p:Project) RETURN count(*)")){ var count = result.Rows().Single()[0]; Try(() => Console.WriteLine((int)count!)); Console.WriteLine((int)(long)count!); Console.WriteLine(Convert.ToInt32(count));}InvalidCastException: Unable to cast object of type 'System.Int64' to type 'System.Int32'.111111count(*) is an INT64, and the row holds a boxed long. Unboxing must name the exact type: (int) on an object is an unboxing, not a numeric conversion. (int)(long)count unboxes, then converts; Convert.ToInt32 does both, and throws OverflowException if the value doesn’t fit. DbDataReader.GetInt32 has the same trap on a bigint column, with a different exception.
Sources
Section titled “Sources”LadybugDBon NuGet,LadybugDB.Native, and theladybug-dotnetrepository at commit0f58f1a- LadybugDB C API header,
lbug.h - Concurrency: read-only and read-write databases, connections
- Data types:
TIMESTAMP,TIMESTAMP_TZ,LIST,STRUCT MATCH: shortest paths- ADO.NET overview, runtime identifiers, NuGet dependency resolution