Today, we’re excited to announce the official .NET binding for EdgeDB!
I crafted the first version of the library a few months ago. Since then I’ve been working closely with(in) the EdgeDB team to fine-tune the implementation and the API to make it an idiomatic EdgeDB client.
Remember to give EdgeDB.Net a ⭐️ on GitHub, check out the docs, and join our Discord!
What makes a good EdgeDB client
All EdgeDB clients share a certain design approach and philosophy that the new .NET client also embraces. In short, all EdgeDB clients should:
-
implement zero-config connectivity, ensured by passing the shared set of connection tests;
-
automatically (and when it’s safe!) reconnect on network errors and retry on transaction serialization errors;
-
be as efficient as possible: lean and mean data serialization and protocol implementation;
-
abstract away the complexity of client-side connection pooling;
-
and, most importantly, the API should be easy to pick up and be productive with in no time!
As usual, we’ve built EdgeDB.Net with performance and developer experience as our highest priorities. EdgeDB.Net achieves this by using a fully asynchronous implementation, making use of high-performance .NET design patterns like Span<T>.
Getting started
To follow along with this demo you will need EdgeDB 🚀. If you are new to EdgeDB you can follow our quickstart guide to get EdgeDB installed and ready.
Make sure that you create the EdgeDB project in your .sln
directory.
This is to ensure that EdgeDB.Net can automatically configure things.
Also keep in mind that the EdgeDB.Net
package targets .NET 6.
Once you have EdgeDB running, install the new driver with the dotnet
command in your terminal:
$
dotnet add package EdgeDB.Net.Driver
A basic client
With the driver installed, you can create a client connection instance with EdgeDB with EdgeDBClient:
using EdgeDB;
var client = new EdgeDBClient();
open EdgeDB;
let client = new EdgeDBClient()
using EdgeDB;
...
services.AddEdgeDB()
To learn more, read our .NET quickstart docs.
Your first query
Now you are ready to run your first query:
var result = await client
.QuerySingleAsync<string>("select 'Hello, .NET!'");
Console.WriteLine(result);
let! result = client.QuerySingleAsync<string>(
"select 'Hello, .NET!'"
)
printf "%s" result
Note that EdgeDB.Net uses the common .NET value types to represent different scalar types in EdgeDB. To see the full type mapping table, check out the datatypes section in our docs.
Advanced data modeling
EdgeDB.Net fully embraces strict typing, allowing you to define concrete types to represent query results. Yet one of the key features of EdgeDB.Net is that it supports polymorphism of EdgeDB types in .NET.
Abstract types defined in EdgeDB schema can be modeled by abstract types in your .NET code. You can then pass an abstract type as a query result and EdgeDB.Net will automatically deserialize data into the correct .NET type.
Let’s first create the .NET types which will map to the types defined in our classic example Movies schema:
module default {
abstract type Content {
required property title -> str;
multi link actors -> Person {
property character_name -> str;
};
};
type Person {
required property name -> str;
link filmography := .<actors[is Content];
};
type Movie extending Content {
property release_year -> int32;
};
type Show extending Content {
property num_seasons := count(.<show[is Season]);
};
type Season {
required link show -> Show;
required property number -> int32;
};
}
public abstract class Content
{
public string? Title { get; set; }
public Person[] Actors { get; set; }
}
public class Person
{
public string? Name { get; set; }
public Content? Filmography { get; set; }
[EdgeDBProperty("@character_name")]
public string CharacterName { get; set; } // link property
}
public class Movie : Content
{
public int ReleaseYear { get; set; }
}
public class Show : Content
{
public long NumSeasons { get; set; }
}
public class Season
{
public Show Show { get; set; }
public int Number { get; set; }
}
type Person = {
Name: string;
[<EdgeDBProperty("@character_name")>]
CharacterName: string;
}
type Movie = {
ReleaseYear: int;
Title: string;
Actors: Person[];
}
type Show = {
NumSeasons: int64;
Title: string;
Actors: Person[];
}
type Season = {
Show: Show;
Number: int;
}
type Content =
| Movie of Movie
| Show of Show
This demo uses a PascalCase naming strategy in .NET types. This strategy is optional and not enabled by default. To learn more about naming strategies and how to enable implicit conversion to your chosen strategy, refer to the Naming Strategy docs.
We can now query our database with the Content
type for the result:
using System.Linq
var content = await client.QueryAsync<Content>(
@"select Content {
title,
actors: {
name,
@character_name
}
}
"
);
var movies = content.Where(x => x is Movie);
var shows = content.Where(x => x is Show);
open System.Linq
let! content = client.QueryAsync<Content>(
"""select Content {
title,
actors: {
name,
@character_name
}
}
""")
let movies = content.Where(fun x -> match x with Movie -> true | _ -> false)
let shows = content.Where(fun x -> match x with Show -> true | _ -> false)
By querying with the Content
abstract type, EdgeDB.Net will return every
Content
object—whether it’s a Movie
or Show
—deserialized as
the corresponding .NET type based on their typename.
To learn more about query result and custom types, check out the Custom Types documentation.
Transactions
EdgeDB.Net supports transactions out of the box, retrying your queries if a retryable error (e.g. a network failure) occurs. If an non-retryable error happens, the queries performed within the transactions are automatically rolled back.
var result = await client.TransactionAsync(async (tx) =>
{
return await tx.QueryRequiredSingleAsync<string>(
"select 'Hello, .NET!'"
);
});
Console.WriteLine(result);
let! result = client.TransactionAsync(fun tx ->
tx.QueryRequiredSingleAsync<string>("select 'Hello, .NET!'")
)
printf "%A" result
Code blocks in transactions may run multiple times. It’s good practice to only perform safe to re-run operations in transaction blocks.
State API
EdgeDB.Net allows to configure state by using the With*()
family of methods.
This allows creating clients with different state configuration while
efficiently sharing the same underlying client pool.
With*
methods will always return a new client instance, which contains
the applied state changes.
This is incredibly useful in tandem with Globals and Access Policies. Let’s use the demo from the access policy docs as an example:
// An example UUID; you should use a real one from your DB!
var userId = Guid.NewGuid();
var scopedClient = client
.WithGlobals(new Dictionary<string, object?>
{
{ "current_user_id", userId }
});
var posts = scopedClients.QueryAsync<BlogPost>(
"select Post { title }"
);
// An example UUID; you should use a real one from your DB!
let userId = Guid.NewGuid()
let scopedClient = client.WithGlobals(
dict [ "current_user_id", userId ]
)
let! posts = scopedClients.QueryAsync<BlogPost>(
"select Post { title }"
)
State API also allows configuring client behavior with extreme granularity:
using EdgeDB.State;
var configuredClient = client
.WithConfig(conf =>
{
conf.AllowDMLInFunctions = true;
conf.ApplyAccessPolicies = true;
conf.DDLPolicy = DDLPolicy.AlwaysAllow;
conf.QueryExecutionTimeout = TimeSpan.FromSeconds(10);
conf.IdleTransationTimeout = TimeSpan.FromSeconds(10);
})
open EdgeDB.State
let configuredClient = client.WithConfig(fun conf ->
conf.AllowDMLInFunctions <- true
conf.ApplyAccessPolicies <- true
conf.DDLPolicy <- DDLPolicy.AlwaysAllow
conf.QueryExecutionTimeout <- TimeSpan.FromSeconds(10)
conf.IdleTransationTimeout <- TimeSpan.FromSeconds(10)
)
See Configuration for state-configuration details.
For more examples using EdgeDB.Net, check out our Github examples repository.
The future of EdgeDB.Net
Whats next for EdgeDB.Net? We’re currently working on a query builder
to provide an EFCore-like feel without the drawbacks of an ORM. You can preview
the beta query builder by installing it via myget
:
$
dotnet add package EdgeDB.Net.QueryBuilder \
--source https://www.myget.org/F/edgedb-net/api/v3/index.json
var person = new Person
{
Email = "example@example.com",
Name = "example"
};
// A complex insert with links & dealing with conflicts
var result = await QueryBuilder
.Insert(new Person
{
BestFriend = person,
Name = "example2",
Email = "example2@example.com"
})
.UnlessConflictOn(x => x.Email)
.ElseReturn()
.ExecuteAsync(client);
More examples using the query builder can be found on our Github.
The query builder is in the very early stage of development. Be advised: bugs are part of the experience and no API is final! 🤓
We’re also working on a codegen
tool to generate .NET code from .edgeql
files. You can read the proposed spec
on github.
Wrapping up
We can’t wait to see what you will build with EdgeDB.Net! ❤️
File feature requests on Github and join the #edgedb-dotnet channel on our Discord to discuss!