.NET/C# client driver for Fauna

Version: 0.1.0-beta Repository: fauna/fauna-dotnet

This driver is in beta and should not be used in production.

Fauna’s .NET/C# client driver lets you run FQL queries from .NET and C# applications.

This guide shows how to set up the driver and use it to run FQL queries. Most of the examples use Fauna’s demo data.

This driver can only be used with FQL v10. It’s not compatible with earlier versions of FQL. To use earlier FQL versions, use the faunadb-csharp package.

Supported .NET and C# versions

  • .NET 6.0

  • .NET 7.0

  • .NET 8.0

  • C# ^10.0

Supported cloud runtimes

  • Azure Functions

Installation

The driver is available on NuGet. To install it using the .NET CLI, run:

dotnet add package Fauna --prerelease

The driver is in beta. Be sure to include the --prerelease flag.

Basic usage

The following applications:

  • Initialize a client instance to connect to Fauna

  • Compose a basic FQL query using an FQL template

  • Run the query using Client.QueryAsync() or Client.PaginateAsync()

  • Deserialize the results based on a provided type parameter

Use Client.QueryAsync() to run a non-paginated query:

using Fauna;
using Fauna.Exceptions;
using static Fauna.Query;

try
{
    // Initialize the client to connect to Fauna
    var client = new Client("YOUR_FAUNA_SECRET");

    // Compose a query
    var query = FQL($@"
        Product.byName('cups').first() {{
            name,
            description,
            price
        }}
    ");

    // Run the query
    // Optionally specify the expected result type as a type parameter.
    // If not provided, the value will be deserialized as object.
    var response = await client.QueryAsync<Dictionary<string, object?>>(query);

    Console.WriteLine(response.Data["name"]);
    Console.WriteLine(response.Data["description"]);
    Console.WriteLine(response.Data["price"]);
    Console.WriteLine("--------");
}
catch (FaunaException e)
{
    Console.WriteLine(e);
}

Queries that return a Set are automatically paginated. Use Client.PaginateAsync() to iterate through paginated results:

using Fauna;
using Fauna.Exceptions;
using static Fauna.Query;

try
{
    // Initialize the client to connect to Fauna
    var client = new Client("YOUR_FAUNA_SECRET");

    // Compose a query
    var query = FQL($@"Store.all() {{ name }}");

    // Run the query
    // PaginateAsync returns an IAsyncEnumerable of pages
    var response = client.PaginateAsync<Dictionary<string, object?>>(query);

    await foreach (var page in response)
    {
        foreach (var product in page.Data)
        {
            Console.WriteLine(product["name"]);
        }
    }
}
catch (FaunaException e)
{
    Console.WriteLine(e);
}

Connect to Fauna

To connect to Fauna, initialize a Client instance using a Fauna key, access token, or JWT:

var client = new Client("YOUR_FAUNA_SECRET");  // Your key, access token, or JWT

Client requires a secret or configuration argument. For configuration options, see Client configuration.

Multiple connections

You can use a single client instance to run multiple asynchronous queries at once. The driver manages HTTP connections as needed. Your app doesn’t need to implement connection pools or other connection management strategies.

You can create multiple client instances to connect to Fauna using different credentials or client configurations.

Run FQL queries

Use FQL templates to compose FQL queries. Run the queries using Client.QueryAsync() or Client.PaginateAsync():

// Unpaginated query
var query = FQL($@"Product.byName('cups').first()");
client.QueryAsync(query);

// Paginated query
// Adjust `pageSize()` size as needed
var paginatedQuery = FQL($@"Store.all().pageSize(2)");
client.PaginateAsync(paginatedQuery);

Variable interpolation

Use single braces {} to pass native variables to fql queries. Use {{}} to escape other single braces in the query.

// Create a native var
var collectionName = "Product";

// Pass the var to an FQL query
var query = FQL($@"
    let collection = Collection({collectionName})
    collection.byName('cups').first() {{ price }}"
);

client.QueryAsync(query);

Passed variables are encoded to an appropriate type and passed to Fauna’s HTTP API. This helps prevent injection attacks.

Subqueries

You can use native variables to pass an FQL query to another FQL query. This lets you create reusable subqueries:

// Create a reusable FQL subquery
Func<string, Query> getProduct = (name) =>
{
    return FQL($@"Product.byName({name}).first()");
};

// Use the subquery in another FQL query
var getPizza = getProduct("pizza");
var query = FQL($@"
    let product = {getPizza}
    product?.update({{
        name: 'pizza pie'
    }})
");

client.QueryAsync(query);

POCO mapping

With Fauna.Mapping.Attributes, you can map a POCO class to a Fauna document or object shape:

using Fauna.Mapping.Attributes;

[Object]
class Product
{
    // Property names are automatically converted to camelCase.
    [Field]
    public string? Id { get; set; }

    // Manually specify a name by providing a string.
    [Field("name")]
    public string? Name { get; set; }

    [Field]
    public string? Description { get; set; }

    [Field]
    public double Price { get; set; }
}

You can use POCO classes to deserialize query responses:

var query = FQL($@"Product.sortedByPriceLowToHigh()");
var products = client.PaginateAsync<Product>(query).FlattenAsync();

await foreach (var p in products)
{
    Console.WriteLine($"{p.Name} {p.Description} {p.Price}");
}

You can also use POCO classes to write to your database:

var product = new Product {
    Id = "12345",
    Name = "limes",
    Description = "Organic, 2 ct",
    Price = 0.95
};

client.QueryAsync(FQL($@"Product.create({product})"));

DataContext

The DataContext class provides a schema-aware view of your database. Subclass it and configure your collections:

class PersonDb : DataContext
{
    public class PersonCollection : Collection<Person>
    {
        public Index<Person> ByFirstName(string first) => Index().Call(first);
        public Index<Person> ByLastName(string last) => Index().Call(last);
    }

    public PersonCollection Person { get => GetCollection<PersonCollection>(); }
}

DataContext provides Client querying, which automatically maps your collections to POCO equivalents, even when type hints are not provided.

var db = client.DataContext<PersonDb>

var result = db.QueryAsync(FQL($"Person.all().first()"));
var person = (Person)result.Data!;

Console.WriteLine(person.FirstName);

LINQ-based queries

The DataContext subclass provides a LINQ-compatible API for type-safe querying:

// 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:

var syncCount = db.Person.Count();
var asyncCount = await db.Person.CountAsync();

Pagination

When you wish to paginate a Set, such as a collection or index, use Client.PaginateAsync().

Example of a query that returns a set:

var query = FQL($"Person.all()");
await foreach (var page in client.PaginateAsync<Person>(query))
{
    // handle each page
}

await foreach (var item in client.PaginateAsync<Person>(query).FlattenAsync())
{
    // handle each item
}

Example of a query that returns an object with an embedded set:

[Object]
class MyResult
{
    [Field("users")]
    public Page<Person>? Users { get; set; }
}

var query = FQL($"{{users: Person.all()}}");
var result = await client.QueryAsync<MyResult>(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 NullDoc can be handled two ways:

  1. Let the driver throw an exception and do something with it:

    try {
        await client.QueryAsync<NamedDocument>(FQL($"Collection.byName('Fake')"))
    } catch (NullDocumentException e) {
        Console.WriteLine(e.Id); // "Fake"
        Console.WriteLine(e.Collection.Name); // "Collection"
        Console.WriteLine(e.Cause); // "not found"
    }
  2. Wrap your expected type in a NullableDocument<>. You can wrap Document, NamedDocument, DocumentRef, NamedDocumentRef, and POCOs.

    var q = FQL($"Collection.byName('Fake')");
    var r = await client.QueryAsync<NullableDocument<NamedDocument>>(q);
    switch (r.Data)
    {
        case NullDocument<NamedDocument> d:
            // Handle the null document case
            Console.WriteLine(d.Id); // "Fake"
            Console.WriteLine(d.Collection.Name); // "Collection"
            Console.WriteLine(d.Cause); // "not found"
            break;
        case NonNullDocument<NamedDocument> d:
            var doc = d.Value!; // NamedDocument
            break;
    }

Query statistics

Successful query responses and ServiceException exceptions include query statistics:

try
{
    var client = new Client("YOUR_FAUNA_SECRET");

    var query = FQL($@"'Hello world'");
    var response = await client.QueryAsync<string>(query);

    Console.WriteLine(response.Stats.ToString());
}
catch (FaunaException e)
{
  if (e is ServiceException serviceException)
  {
    Console.WriteLine(serviceException.Stats.ToString());
    Console.WriteLine(e);
  }
  else {
    Console.WriteLine(e);
  }
}

Client configuration

The Client instance comes with reasonable configuration defaults. We recommend using the defaults in most cases.

If needed, you can configure the client and override the defaults. This also lets you set default Query options.

var config = new Configuration("YOUR_FAUNA_SECRET")
{
    // Configure the client
    Endpoint = new Uri("https://db.fauna.com"),
    RetryConfiguration = new RetryConfiguration(3, TimeSpan.FromSeconds(20)),

    // Set default query options
    DefaultQueryOptions = new QueryOptions
    {
        Linearized = false,
        QueryTags = new Dictionary<string, string>
        {
            { "name", "hello world query" }
        },
        QueryTimeout = TimeSpan.FromSeconds(60),
        TraceParent = "00-750efa5fb6a131eb2cf4db39f28366cb-000000000000000b-00",
        TypeCheck = false
    }
};

var client = new Client(config);

The following table outlines properties for the Configuration class and their defaults.

Property Type Required Description

Endpoint

Uri

URL for the Fauna endpoint. Defaults to https://db.fauna.com.

RetryConfiguration

Configures retry logic and exponential backoff for requests. See Retries.

DefaultQueryOptions

Configures default query options. See Query options.

Retries

By default, the client automatically retries a query if the request returns a 429 HTTP status code. Retries use an exponential backoff.

The client retries a query up to three times by default. The maximum wait time between retries defaults to 20 seconds.

To override these defaults, pass a RetryConfiguration instance to the Client configuration.

var config = new Configuration("YOUR_FAUNA_SECRET")
{
    RetryConfiguration = new RetryConfiguration(3, TimeSpan.FromSeconds(20))
};

var client = new Client(config);

The following table outlines parameters for the RetryConfiguration() constructor.

Parameter Type Required Description

retryCount

int

Yes

Maximum number of retry attempts for a query.

maxBackoff

TimeSpan

Yes

Maximum time to wait before retrying a query if the request returns a 429 HTTP status code.

Query options

The Client configuration sets default query options for the following methods:

  • Client.QueryAsync()

  • Client.PaginateAsync()

You can pass a QueryOptions argument to override these defaults:

var queryOptions = new QueryOptions
{
    Linearized = false,
    QueryTags = new Dictionary<string, string>
    {
        { "name", "hello world query" }
    },
    QueryTimeout = TimeSpan.FromSeconds(60),
    TraceParent = "00-750efa5fb6a131eb2cf4db39f28366cb-000000000000000b-00",
    TypeCheck = true
};

var query = FQL($@"'Hello world'");
client.QueryAsync(query, queryOptions);

The following table outlines properties for the QueryOptions class and their defaults.

Property Type Required Description

Linearized

bool

If true, queries are linearized, ensuring strict serialization of reads and writes. Defaults to null (false).

QueryTags

Dictionary<string, string>

Key-value tags used to identify the query. Defaults to null (none).

Query tags are included in query logs and the response body for successful queries. The tags are typically used for monitoring.

QueryTimeout

TimeSpan

Maximum amount of time Fauna runs a query before marking it as failed. Defaults to Fauna’s default query execution time (2 minutes).

TraceParent

string

W3C-compliant traceparent ID for the request. If you provide an invalid traceparent ID, Fauna generates a valid one. Defaults to null (none).

The traceparent ID is included in query logs. Traceparent IDs are typically used for monitoring.

TypeCheck

bool

If true, enables type checking for queries. Defaults to the database’s type checking setting.

If true, type checking must be enabled on the database.

Is this article helpful? 

Tell Fauna how the article can be improved:
Visit Fauna's forums or email docs@fauna.com

Thank you for your feedback!