Doxense / foundationdb-dotnet-client

C#/.NET Binding for FoundationDB Client API
Other
145 stars 31 forks source link
distributed-database dotnet dotnetcore fdb foundationdb key-value-store

FoundationDB .NET Client

C#/.NET binding for the FoundationDB client library.

Build status Actions Status

How to use

You will need to install two things:

Using Dependency Injection

Manual configuration

In your Program.cs, you should register FoundationDB with the DI container:

using FoundationDB.Client; // this is the main namespace of the library

var builder = WebApplication.CreateBuilder(args);

// ...

// hook-up the various FoundationDB types and interfaces with the DI container
// You MUST select the appropriate API level that matches the target cluster (here '710' requires at least v7.1)
builder.Services.AddFoundationDb(710, options =>
{
    // auto-start the connection to the cluster on the first request
    options.AutoStart = true; 

    //you can configure additional options here, like the path to the .cluster file, default timeouts, ...
});

var app = builder.Build();

// ...

// note: you don't need to configure anything for this step

app.Run();

This will register an instance of the IFdbDatabaseProvider singleton, that you can then inject into your controllers, razor pages or any other services.

Let say, for example, that we have a Books Razor Page, that is reachable via the /Books/{id} route:


namespace MyWebApp.Pages
{

    using FoundationDB.Client;

    /// <summary>Represent a Book that will be stored (as JSON) into the database</summary>
    public sealed record Book
    {
        public required string Id { get; init; }

        public required string Title { get; init; }

        public required string ISBN { get; init; }

        public required string AuthorId { get; init; }

        // ...

    }

    /// <summary>This page is used to display the details of a specific book</summary>
    /// <remarks>Accessible via the route '/Books/{id}'</remarks>
    public class BooksModel : PageModel
    {

        public BooksModel(IFdbDatabaseProvider db)
        {
            this.Db = db;
        }

        private IFdbDatabaseProvider Db { get; }

        public Book Book { get; private set; }

        public async Task OnGet(string id, CancellationToken ct)
        {
            // perform parameter validation, ACL checks, and any pre-processing here

            // start a read-only retry-loop
            Slice jsonBytes = await this.Db.ReadAsync((IFdbReadOnlyTransaction tr) =>
            {
                // Read the value of the ("Books", <ID>) key
                Slice value = await tr.GetAsync(TuPack.Pack(("Books", id)));

                // the transaction can be used to read additional keys and ranges,
                // and has a lifetime of max. 5 secondes.

                return value;
            }, ct);

            // here you can perform any post-processing of the result, outside of the retry-loop

            // if the key does not exist in the database, GetAsync(...) will return Slice.Nil
            if (jsonBytes.IsNull)
            {
                // This book does not exist, return a 404 page to the browser!
                return NotFound();
            }

            // If the key exists, then GetAsync(...) will return its value as bytes, that can be deserialized
            Book book = JsonSerializer.Deserialize<Book>(jsonBytes.Span);

            // perform any checks and validation here, like converting the Model (from the database) into a ViewModel (for the razor template)

            this.Book = book;
        }
    }
}

Using Aspire

Note: Aspire is currently in preview, so things may change at any time!

At the time of this writing, you need at least Preview 4 (8.0.0-preview.4.24156.9) of the Aspire SDK !

It is possible to add a FoundationDB cluster resource to your Aspire application model, and pass a reference to this cluster to the projects that need it.

For local development, a local FoundationDB node will be started using the foundationdb/foundationdb Docker image, and all projects that use the cluster reference will have a temporary Cluster file pointing to the local instance.

Note: you will need to install Docker on your development machine, as explained in https://learn.microsoft.com/en-us/dotnet/aspire/get-started/add-aspire-existing-app#prerequisites

In the Program.cs of you AppHost project:

private static void Main(string[] args)
{
    var builder = DistributedApplication.CreateBuilder(args);

    // Define a locally hosted FoundationDB cluster
    var fdb = builder
        .AddFoundationDb("fdb", apiVersion: 720, root: "/Sandbox/MySuperApp", clusterVersion: "7.2.5", rollForward: FdbVersionPolicy.Exact);

    // Project that needs a reference to this cluster
    var backend = builder
        .AddProject<Projects.AwesomeWebApiBackend>("backend")
        //...
        .WithReference(fdb); // register the fdb cluster connection

    // ...
}

For testing/staging/production, or "non local" development, it is also possible to configure a FoundationDB connection resource that will pass the specified Cluster file to the projects that reference the cluster resource.

In the Program.cs of your AppHost project:

private static void Main(string[] args)
{
    var builder = DistributedApplication.CreateBuilder(args);

    // Define an external FoundationDB cluster connection
    var fdb = builder
        .AddFoundationDbCluster("fdb", apiVersion: 720, root: "/Sandbox/MySuperApp", clusterFile: "/SOME/PATH/TO/testing.cluster")      ;

    // Project that needs a reference to this cluster
    var backend = builder
        .AddProject<Projects.AwesomeWebApiBackend>("backend")
        //...
        .WithReference(fdb); // register the fdb cluster connection

    // ...
}

Then, in the Program.cs, or where you are declaring your services with the DI, use the following extension method to add support for FoundationDB:

var builder = WebApplication.CreateBuilder(args);

// setup Aspire services...
builder.AddServiceDefaults();
//...

// hookup the FoundationDB component
builder.AddFoundationDb("fdb"); // "fdb" is the same name we used in AddFoundationDb(...) or AddFoundationDbCLuster(...) in the AppHost above.

// ...rest of the startup logic....

This will automatically register an instance of the IFdbDatabaseProvider service, automatically configured to connect the FDB local or external cluster defined in the AppHost.

Using the Directory Layer

Please note that in real use case, it is highly encourage to use the Directory Layer to generate a prefix for the keys, instead of simply using the ("Books", ...) prefix.

In your startup logic:


public sealed class BookOptions
{

    /// <summary>Path to the root directory subspace of the application where all data will be stored</summary>
    public FdbPath Location { get; set; } // ex: "/Tenants/ACME/MyApp/v1"

}

// ...

builder.Services.Configure<BookOptions>(options =>
{
    // note: this would be read from your configuration!
    options.Location = FdbPath.Relative("Tenants", "ACME", "MyApp", "v1");
});

In your Razor Page:

public class BooksModel : PageModel
{

    public BooksModel(IOptions<BookOptions> options, IFdbDatabaseProvider db)
    {
        this.Options = options;
        this.Db = db;
    }

    private IFdbDatabaseProvider Db { get; }

    private IOptions<BookOptions> Options { get; }

    public async Task OnGet(string id, CancellationToken ct)
    {
        Slice jsonBytes = await this.Db.ReadAsync((IFdbReadOnlyTransaction tr) =>
        {
            // get the location that corresponds to this path
            var location = this.Db.Root[this.Options.Value.Location];

            // "resolve" this location into a Directory Subspace that will add the matching prefix to our keys
            var subspace = await location.Resolve(tr);

            // use this subspace to generate our keys
            Slice value = await tr.GetAsync(subspace.Encode("Books", id));

            // ....

        }

        // ...

    }

}

Access the underlying IFdbDatabase singleton

The IFdbDatabaseProvider also has a GetDatabase(...) method that can be used to obtain an instance of the IFdbDatabase singleton, that can then be used directly, or passed to any other Layer or library.

public class FooBarModel : PageModel
{

    public FooBarModel(IFdbDatabaseProvider db, IFooBarLayer layer)
    {
        this.Db = db;
        this.Layer = layer;
    }

    private IFdbDatabaseProvider Db { get; }

    private IFooBarLayer Layer { get; }

    public List<Foo> Results { get; }

    public async Task OnGet(...., CancellationToken ct)
    {
        // get an instance of the database singleton
        var db = await this.Db.GetDatabase(ct);
        // notes:
        // - if AutoStart is false, this will throw an exception if the provider has not been started manually during starting.
        // - if AutoStart is true, the very first call will automatically start the connection.
        // - Once the connection has been established, calls to GetDatabase will return an already-completed task with a cached singleton (or exception).

        // call some method on this layer, that will perform a query on the database and return a list of results
        this.Results = await this.Layer.Query(db, ...., ct);
    }

}

Hosting

Hosting on ASP.NET Core / Kestrel

Here is an easy way to inject the client binary in a Dockerfile:

# Version of the FoundationDB Client Library
ARG FDB_VERSION=7.2.9

# We will need the official fdb docker image to obtain the client binaries
FROM foundationdb/foundationdb:${FDB_VERSION} as fdb

FROM mcr.microsoft.com/dotnet/aspnet:7.0

# copy the binary from the official fdb image into our target image.
COPY --from=fdb /usr/lib/libfdb_c.so /usr/lib

WORKDIR /App

COPY . /App

ENTRYPOINT ["dotnet", "MyWebApp.dll"]

Hosting on IIS

Hosting on OWIN

How to build

Visual Studio Solution

You will need Visual Studio 2022 version 17.5 or above to build the solution (C# 12 and .NET 8.0 support is required).

You will also need to obtain the 'fdb_c.dll' C API binding from the foundationdb.org website, by installing the client SDK:

From the Command Line

You can also build, test and compile the NuGet packages from the command line, using FAKE.

You will need to perform the same steps as above (download and install FoundationDB)

In a new Command Prompt, go the root folder of the solution and run one of the following commands:

If you get System.UnauthorizedAccessException: Access to the path './build/output/FoundationDB.Tests\FoundationDB.Client.dll' is denied. errors from time to time, you need to kill any nunit-agent.exe process that may have stuck around.

How to test

The test project is using NUnit 3.x.

If you are using a custom runner or VS plugin (like TestDriven.net), make sure that it has the correct nunit version, and that it is configured to run the test using 64-bit process. The code will NOT work on 32 bit.

WARNING: All the tests should work under the ('T',) subspace, but any bug or mistake could end up wiping or corrupting the global keyspace and you may lose all your data. You can specify an alternative cluster file to use in TestHelper.cs file.

Implementation Notes

Please refer to https://apple.github.io/foundationdb/ to get an overview on the FoundationDB API, if you haven't already.

This .NET binding has been modeled to be as close as possible to the other bindings (Python especially), while still having a '.NET' style API.

There were a few design goals, that you may agree with or not:

However, there are some key differences between Python and .NET that may cause problems:

The following files will be required by your application

Known Limitations

License

This code is licensed under the 3-clause BSD License.

Contributing