https://github.com/fauna/fauna-dotnet
Fauna FQL v10 driver for C#
https://github.com/fauna/fauna-dotnet
Last synced: 4 months ago
JSON representation
Fauna FQL v10 driver for C#
- Host: GitHub
- URL: https://github.com/fauna/fauna-dotnet
- Owner: fauna
- License: mpl-2.0
- Created: 2023-11-21T00:13:54.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2025-01-28T15:00:22.000Z (over 1 year ago)
- Last Synced: 2025-07-21T07:32:00.440Z (11 months ago)
- Language: C#
- Size: 6.33 MB
- Stars: 1
- Watchers: 10
- Forks: 1
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Official .NET Driver for [Fauna v10](https://fauna.com/) (current)
This driver can only be used with FQL v10, and is not compatible with earlier versions of FQL. To query your databases with earlier API versions, see [faunadb-csharp](https://github.com/fauna/faunadb-csharp).
See the [Fauna Documentation](https://docs.fauna.com/fauna/current/) for additional information about how to configure and query your databases.
## Features
- Injection-safe query composition with interpolated string templates
- POCO-based data mapping
- Async LINQ API for type-safe querying
## Compatibility
- C# ^10.0
- .NET 8.0
## Installation
Using the .NET CLI:
```
dotnet add package Fauna
```
## API reference
API reference documentation for the driver is available at
https://fauna.github.io/fauna-dotnet/. The docs are generated using
[Doxygen](https://www.doxygen.nl/).
## Basic usage
```csharp
using Fauna;
using Fauna.Exceptions;
using static Fauna.Query;
class Basics
{
static async Task Main()
{
try
{
// The client's authentication secret
// defaults to the `FAUNA_SECRET` env var.
var client = new Client();
var hi = "Hello, Fauna!";
// The FQL template function safely interpolates values.
var helloQuery = FQL($@"
let x = {hi}
x");
// Optionally specify the expected result type as a type parameter.
// If not provided, the value will be deserialized as object?
var hello = await client.QueryAsync(helloQuery);
Console.WriteLine(hello.Data); // Hello, Fauna!
Console.WriteLine(hello.Stats.ToString()); // compute: 1, read: 0, write: 0, ...
var peopleQuery = FQL($@"Person.all() {{ first_name }}");
// PaginateAsync returns an IAsyncEnumerable of pages
var people = client.PaginateAsync>(peopleQuery);
await foreach (var page in people)
{
foreach (var person in page.Data)
{
Console.WriteLine($"Hello, {person["first_name"]}!"); // Hello, John! ...
}
}
}
catch (FaunaException e)
{
// Handle exceptions
}
}
}
```
## Writing more complex queries
The FQL template DSL supports arbitrary composition of subqueries along with values.
```csharp
var client = new Client();
var predicate = args[0] switch {
"first" => FQL($".first_name == {args[1]}"),
"last" => FQL($".last_name" == {args[1]}),
_ => throw ArgumentException(),
};
// Single braces are for template variables, so escape them with double braces.
var getPerson = FQL($"Person.firstWhere({predicate}) {{ id, first_name, last_name }}");
// Documents can be mapped to Dictionaries as well as POCOs (see below)
var result = await client.QueryAsync>(getPerson);
Console.WriteLine(result.Data["id"]);
Console.WriteLine(result.Data["first_name"]);
Console.WriteLine(result.Data["last_name"]);
```
## Database contexts and POCO data mapping
Fauna.Mapping.Attributes and the Fauna.DataContext class provide the ability to bring your Fauna database schema into your code.
### POCO Mapping
You can use attributes to map a POCO class to a Fauna document or object shape:
* `[Id]`: Should only be used once per class on a field that represents the Fauna document ID. It's not encoded unless the isClientGenerated flag is true.
* `[Ts]`: Should only be used once per class on a field that represents the timestamp of a document. It's not encoded.
* `[Collection]`: Typically goes unmodeled. Should only be used once per class on a field that represents the collection field of a document. It will never be encoded.
* `[Field]`: Can be associated with any field to override its name in Fauna.
* `[Ignore]`: Can be used to ignore fields during encoding and decoding.
```csharp
using Fauna.Mapping;
class Person
{
// Property names are automatically converted to camelCase.
[Id]
public string? Id { get; set; }
// Manually specify a name by providing a string.
[Field("first_name")]
public string? FirstName { get; set; }
[Field("last_name")]
public string? LastName { get; set; }
public int Age { get; set; }
}
```
Your POCO classes can be used to drive deserialization:
```csharp
var peopleQuery = FQL($@"Person.all()");
var people = client.PaginateAsync(peopleQuery).FlattenAsync();
await foreach (var p in people)
{
Console.WriteLine($"{p.FirstName} {p.LastName}");
}
```
As well as to write to your database:
```csharp
var person = new Person { FirstName = "John", LastName = "Smith", Age = 42 };
var result = await client.QueryAsync($@"Person.create({person}).id");
Console.WriteLine(result.Data); // 69219723210223...
```
### DataContext
The DataContext class provides a schema-aware view of your database. Subclass it and configure your collections:
```csharp
class PersonDb : DataContext
{
public class PersonCollection : Collection
{
public Index ByFirstName(string first) => Index().Call(first);
public Index ByLastName(string last) => Index().Call(last);
}
public PersonCollection Person { get => GetCollection(); }
public int AddTwo(int val) => Fn().Call(val);
public async Task TimesTwo(int val) => await Fn("MultiplyByTwo").CallAsync(val);
}
```
DataContext provides Client querying which automatically maps your collections' documents to their POCO equivalents even when type hints are not provided.
```csharp
var db = client.DataContext
var result = db.QueryAsync($"Person.all().first()");
var person = (Person)result.Data!;
Console.WriteLine(person.FirstName);
```
### LINQ-based queries (preview)
> [!IMPORTANT]
> This functionality is in preview and may change in future releases.
DataContext provides a LINQ-compatible API for type-safe querying.
```csharp
// general query
db.Person.Where(p => p.FirstName == "John")
.Select(p => new { p.FirstName, p.LastName })
.First();
// or start with an index
db.Person.ByFirstName("John")
.Select(p => new { p.FirstName, p.LastName })
.First();
```
There are async variants of methods which execute queries:
```csharp
var syncCount = db.Person.Count();
var asyncCount = await db.Person.CountAsync();
```
## Paginating [Fauna Sets](https://docs.fauna.com/fauna/current/reference/reference/schema_entities/set/)
When you wish to paginate a Set, such as a Collection or Index, use `PaginateAsync`.
Example of a query that returns a Set:
```csharp
var query = FQL($"Person.all()");
await foreach (var page in client.PaginateAsync(query))
{
// handle each page
}
await foreach (var item in client.PaginateAsync(query).FlattenAsync())
{
// handle each item
}
```
Example of a query that returns an object with an embedded Set:
```csharp
class MyResult
{
[Field("users")]
public Page? Users { get; set; }
}
var query = FQL($"{{users: Person.all()}}");
var result = await client.QueryAsync(query);
await foreach (var page in client.PaginateAsync(result.Data.Users!))
{
// handle each page
}
await foreach (var item in client.PaginateAsync(result.Data.Users!).FlattenAsync())
{
// handle each item
}
```
## Null Documents
A null document ([NullDoc](https://docs.fauna.com/fauna/current/reference/fql_reference/types#nulldoc)) can be handled two ways.
Option 1, you can let the driver throw an exception and do something with it.
```csharp
try {
await client.QueryAsync(FQL($"SomeColl.byId('123')"))
} catch (NullDocumentException e) {
Console.WriteLine(e.Id); // "123"
Console.WriteLine(e.Collection.Name); // "SomeColl"
Console.WriteLine(e.Cause); // "not found"
}
```
Option 2, you wrap your expected type in a Ref<> or NamedRef<>. Supported types are Dictionary and POCOs.
```csharp
var q = FQL($"Collection.byName('Fake')");
var r = (await client.QueryAsync>>(q)).Data;
if (r.Data.Exists) {
Console.WriteLine(d.Id); // "Fake"
Console.WriteLine(d.Collection.Name); // "Collection"
var doc = r.Get(); // A dictionary with id, coll, ts, and any user-defined fields.
} else {
Console.WriteLine(d.Name); // "Fake"
Console.WriteLine(d.Collection.Name); // "Collection"
Console.WriteLine(d.Cause); // "not found"
r.Get() // this throws a NullDocumentException
}
```
## Event feeds (beta)
The driver supports [event
feeds](https://docs.fauna.com/fauna/current/learn/cdc/#event-feeds).
An event feed asynchronously polls an [event
source](https://docs.fauna.com/fauna/current/learn/cdc/#create-an-event-source)
for events.
To get paginated events, pass an [event
source](https://docs.fauna.com/fauna/current/learn/cdc/#create-an-event-source)
or a query that produces an event source to `EventFeedAsync()`:
```csharp
// Get an event source from a supported Set
EventSource eventSource = await client.QueryAsync(FQL($"Person.all().eventSource()"));
// Calculate timestamp for 10 minutes ago in microseconds
long tenMinutesAgo = DateTimeOffset.UtcNow.AddMinutes(-10).ToUnixTimeMilliseconds() * 1000;
var feedOptions = new FeedOptions(startTs: tenMinutesAgo, pageSize: 10);
// Pass the event source and `FeedOptions` to `EventFeedAsync()`:
var feed = await client.EventFeedAsync(eventSource, feedOptions);
// You can also pass a query that produces an event source directly to `EventFeedAsync()`:
var feedFromQuery = await client.EventFeedAsync(FQL($"Person.all().eventsOn({{ .price, .stock }})"), feedOptions);
// EventFeedAsync() returns a `FeedEnumerable` instance that can act as an `AsyncEnumerator`.
// Use `foreach()` to iterate through the pages of events.
await foreach (var page in feed)
{
foreach (var evt in page.Events)
{
Console.WriteLine($"Event Type: {evt.Type}");
Person person = evt.Data;
Console.WriteLine($"First Name: {person.FirstName} - Last Name: {person.LastName} - Age: {person.Age}");
}
}
```
## Event streams
The driver supports [event streams](https://docs.fauna.com/fauna/current/learn/cdc/#event-streaming).
To start and subscribe to an event stream, pass a query that produces an [event
source](https://docs.fauna.com/fauna/current/learn/cdc/#create-an-event-source)
to `EventStreamAsync()`:
```csharp
var stream = await client.EventStreamAsync(FQL($"Person.all().eventSource()"));
await foreach (var evt in stream)
{
Console.WriteLine($"Received Event Type: {evt.Type}");
if (evt.Data != null) // Status events won't have Data
{
Person person = evt.Data;
Console.WriteLine($"First Name: {person.FirstName} - Last Name: {person.LastName} - Age: {person.Age}");
}
}
```
## Debug logging
To enable debug logging, set the `FAUNA_DEBUG` environment variable to an integer for the `Microsoft.Extensions.Logging.LogLevel`. For example:
* `0`: `LogLevel.Trace` and higher (all messages)
* `3`: `LogLevel.Warning` and higher
The driver logs HTTP request and response details, including headers. For security, the `Authorization` header is redacted in debug logs but is visible in trace logs.
> [!NOTE]
> As of v1.0.0, the driver only outputs `LogLevel.Debug` messages. Use `0` (Trace) or `1` (Debug) to log these messages.
For advanced logging, you can use a custom `ILogger` implementation, such as Serilog or NLog. Pass the implementation to the `Configuration` class when instantiating a `Client`.
### Basic example: Serilog
Install the packages:
```
$ dotnet add package Serilog
$ dotnet add package Serilog.Extensions.Logging
$ dotnet add package Serilog.Sinks.Console
$ dotnet add package Serilog.Sinks.File
```
Configure and use the logger:
```csharp
using Fauna;
using Microsoft.Extensions.Logging;
using Serilog;
using static Fauna.Query;
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Verbose()
.WriteTo.Console()
.WriteTo.File("log.txt",
rollingInterval: RollingInterval.Day,
rollOnFileSizeLimit: true)
.CreateLogger();
var logFactory = new LoggerFactory().AddSerilog(Log.Logger);
var config = new Configuration("mysecret", logger: logFactory.CreateLogger("myapp"));
var client = new Client(config);
await client.QueryAsync(FQL($"1+1"));
// You should see LogLevel.Debug messages in both the Console and the "log{date}.txt" file
```