.NET driver for ArangoDB with support for database per tenant deployments


Keywords
arangodb, driver, arangodb-client, csharp, database, graph, linq, netcore
License
Apache-2.0
Install
Install-Package Core.Arango -Version 3.8.320

Documentation

Build Nuget Nuget

dotnet add package Core.Arango

.NET driver for ArangoDB

  • .NET Standard 2.0, 2.1, .NET 8 and .NET 9 driver for ArangoDB 3.11+
  • LINQ support (WIP)
  • Newtonsoft.Json and System.Text.Json serialization support with PascalCase and camelCase options
  • Updates from anonymous types supported as (Id, Key, Revision, From, To) properties are translated to (_id, _key, _rev, _from, _to)
    • This means these property names are reserved and cannot be used for something else (e.g. "To" property in email collection)

Extensions

This driver has various extensions available.

Extension Nuget Command
Core.Arango.Migration Nuget Nuget dotnet add package Core.Arango.Migration
Core.Arango.DataProtection Nuget Nuget dotnet add package Core.Arango.DataProtection
Core.Arango.DevExtreme Nuget Nuget dotnet add package Core.Arango.DevExtreme
Core.Arango.Serilog Nuget Nuget dotnet add package Core.Arango.Serilog

Examples

Initialize context

  • Realm optionally prefixes all further database handles (e.g. "myproject-database")

  • Context is completely thread-safe and can be shared for your whole application

    // from connection string
    var arango = new ArangoContext("Server=http://localhost:8529;Realm=myproject;User=root;Password=;");
    
    // from connection string - NO_AUTH
    var arango = new ArangoContext("Server=http://localhost:8529;");
    
    // from connection string with PascalCase serialization
    var arango = new ArangoContext("Server=http://localhost:8529;Realm=myproject;User=root;Password=;",
    new ArangoConfiguration
    {
        Serializer = new ArangoJsonSerializer(new ArangoJsonDefaultPolicy())
    });
  • For AspNetCore DI extension is available:

    public void ConfigureServices(IServiceCollection services)
    {
        // add with connection string
        services.AddArango(Configuration.GetConnectionString("Arango"));
      
        // or add with custom configuration
        services.AddArango((sp, config) =>
        {
            config.ConnectionString = Configuration.GetConnectionString("Arango");
            config.Serializer = new ArangoJsonSerializer(new ArangoJsonDefaultPolicy());
          
            var logger = sp.GetRequiredService<ILogger<Startup>>();
    
            config.QueryProfile = (query, bindVars, stats) =>
            {
                var boundQuery = query;
    
                // replace parameters with bound values
                foreach (var p in bindVars.OrderByDescending(x => x.Key.Length))
                    boundQuery = boundQuery.Replace("@" + p.Key, JsonConvert.SerializeObject(p.Value));
    
                logger.LogInformation(boundQuery);
            }
        });
    }
    [ApiController]
    [Route("api/demo")]
    public class DemoController : Controller 
    {
        private readonly IArangoContext _arango;
    
        public DemoController(IArangoContext arango)
        {
            _arango = arango;
        }
    }

Serializer definition

The specific serializer can be configured on setting the Arango configuration when creating the context.

Supported serializer:

  • Microsoft System.Text.Json

    using Core.Arango.Serialization.Json;
    
    // Specify with PascalCase
    Serializer = new ArangoJsonSerializer(new ArangoJsonDefaultPolicy());
    
    // Specify with camelCase
    Serializer = new ArangoJsonSerializer(new ArangoJsonCamelCasePolicy());
  • Newtonsoft.Json

    using Core.Arango.Serialization.Newtonsoft;
    
    // Specify with PascalCase
    Serializer = new ArangoNewtonsoftSerializer(new ArangoNewtonsoftDefaultContractResolver()) 
    
    // Specify with CamelCase
    Serializer = new ArangoNewtonsoftSerializer(new ArangoNewtonsoftCamelCaseContractResolver())

Serialize DateTime / DateTimeOffset to Unix Timestamp

  • Microsoft System.Text.Json

    using Core.Arango.Serialization.Json;
    
    // Model example: uses DateTimeOffset
    private class DateTimeOffsetEntity
    {
        public DateTimeOffset A { get; set; }
        public DateTimeOffset B { get; set; }
    
    }
    
    // Define serializer on Arango context
    Serializer = new ArangoJsonSerializer(new ArangoJsonDefaultPolicy())
    {
        UseTimestamps = true // Serialize DateTime / DateTimeOffset to Unix Timestamp (in milliseconds)
    };
    
    // Use
    await arango.Document.CreateAsync("database", "collection", instanceOfDateTimeOffsetEntity); // Converts between Unix Timestamp in DB and C# data type
  • Newtonsoft.Json

    // not supported

Create database

await arango.Database.CreateAsync("database");

Create collection

await arango.Collection.CreateAsync("database", "collection", ArangoCollectionType.Document);

Collection with keys in ascending lexicographical sort order (ideal for log/audit collections)

await arango.Collection.CreateAsync("database", new ArangoCollection
{
    Name = "paddedcollection",
    Type = ArangoCollectionType.Document,
    KeyOptions = new ArangoKeyOptions
    {
        Type = ArangoKeyType.Padded,
        AllowUserKeys = false
    }
});

Create document

await arango.Document.CreateAsync("database", "collection", new
{
    Key = Guid.NewGuid(),
    SomeValue = 1
});

Get documents

Retrieve documents from a single collection based on a LinQ statement. See LinQ help for more information.

var list1 = await Arango.Query<Entity>("database").Where(p => p.Id == "myid").ToListAsync();

Get (many) documents by providing a list of keys or objects with "Key" and optional "Revision" property

var list1 = await Arango.Document.GetManyAsync<Entity>("database", "collection", new List<string> {
  "1", "2"
});

var list2 = await Arango.Document.GetManyAsync<Entity>("database", "collection", new List<object>
{
  new
  {
    Key = "1"
  },
  new
  {
    Key = "2"
  }
});

Update document

await arango.Document.UpdateAsync("database", "collection", new
{
    Key = Guid.Parse("some-guid"),
    SomeValue = 2
});

Ignore specific properties

// depending on serializer
using System.Text.Json.Serialization;
// or
using Newtonsoft.Json;

class ComplexEntity
{
    public string Key { get; set; }
    public string Name { get; set; }
    
    // Will never be read or written from or to arangodb
    [JsonIgnore]
    public object Data { get; set; }
    
    // Newtonsoft only
    // Will only be read from query, on write will be ignored
    [ArangoIgnore]
    public object CalculatedProperty { get; set; }
}

await arango.Document.UpdateAsync("database", "collection", new ComplexEntity {
    Key = "123",
    Name = "SomeName"
});

Custom Query

Bindable parameters

var col = "collection";
var list = new List<int> {1, 2, 3};

// System.Text.Json
var result = await arango.Query.ExecuteAsync<JsonObject>("database",
  $"FOR c IN {col:@} FILTER c.SomeValue IN {list} RETURN c");

// Newtonsoft.Json
var result = await arango.Query.ExecuteAsync<JObject>("database",
  $"FOR c IN {col:@} FILTER c.SomeValue IN {list} RETURN c");

Results in AQL injection save syntax:

'FOR c IN @@C1 FILTER c.SomeValue IN @P2 RETURN c'

{
  "@C1": "collection",
  "P2": [1, 2, 3]
}

for collections parameters, formats '@', 'C' and 'c' are supported. They all mean the same format.

Split queries into parts

var collectionName = "collection";
var list = new List<int> {1, 2, 3};

FormattableString forPart = $"FOR c IN {collectionName:@}";
FormattableString filterPart = $"FILTER c.SomeValue IN {list}";
FormattableString returnPart = $"RETURN c";

// System.Text.Json
var result = await arango.Query.ExecuteAsync<JsonObject>("database",
  $"{forPart} {filterPart} {returnPart}");

// Newtonsoft.Json
var result = await arango.Query.ExecuteAsync<JObject>("database",
  $"{forPart} {filterPart} {returnPart}");

Note
If using multiple FormattableString variables, every single injected string variable needs to be of type FormattableString or Arango will return an error stating:

AQL: syntax error, unexpected bind parameter near @P3

Inject data into Arango return

public class MyClass2
{
    public MyClass Data { get; set; }
    public double CustomData { get; set; }
}

var collectionName = "collection";
var list = new List<int> {1, 2, 3};
var pointA = "[48.157741, 11.503159]";
var pointB = "[48.155739, 11.491601]";

FormattableString forPart = $"FOR c IN {collectionName:@}";
FormattableString filterPart = $"FILTER c.SomeValue IN {list}";
FormattableString letDistance = $"LET distance = GEO_DISTANCE({pointA}, {pointB})";
FormattableString returnPart = $"RETURN {{Data: c, Distance: distance}}";

var result = await arango.Query.ExecuteAsync<MyClass2>("database",
  $"{forPart} {filterPart} {letDistance} {returnPart}");

Query with async enumerator

// insert 100.000 entities 
await Arango.Document.CreateManyAsync("database", "collection", Enumerable.Range(1, 100000).Select(x => new Entity { Value = x }));

// iterate in batches over 100.000 entity ids
await foreach (var x in Arango.Query.ExecuteStreamAsync<string>("database", $"FOR c IN collection RETURN c._id"))
{
    Process(x)
}

Linq

LINQ support has been adapted from https://github.com/ra0o0f/arangoclient.net. Internalized re-motion relinq since their nuget is quite outdated

Work in progress as some things are deprecated or need to be modernized

  • Basic queries generally work
  • Some more complex queries (chaining multiple operators, complex subqueries, etc.) are not supported yet
  • All these issues are solvable and pull requests are accepted
  • NOTE : Some queries will blow up at run time and you will get an exception, but some queries will actually generate valid AQL with the wrong semantics.
  • Combining multiple result operators is not recommended with the current implementation (e.g. .Intersect(list).Count() will generate valid AQL but will apply the operators in the wrong order)
  • The following operators are not yet supported:
    • .Average
    • .Cast
    • .Distinct
    • .GroupJoin
    • .Intersect
    • .Join
    • .Last
    • .Reverse
  • All other operators should be supported. If you find any queries that fail or generate incorrect AQL, open an issue so we can at least document it. A PR with a (failing) unit test would be even better!
  • There is also development done on a driver without relinq and aggregate support
  • Configurable property / collection / group naming for camelCase support

Simple query

var list1 = await Arango.Query<Project>("database").Where(p => p.Id == "myid").ToListAsync();

AQL debug

var q = Arango.Query<Project>("database").Where(p => p.Id == "myid");

// Execute
await q.ToListAsync();

// Debug
var (aql, bindVars) = q.ToAql();

DOCUMENT() lookup

var q = Arango.Query<Project>("test")
.Where(x => x.Name == "Project A")
.Select(x => new
{
    x.Key,
    x.Name,
    ClientName = Aql.Document<Client>("Client", x.ClientKey).Name
});

// Execute
await q.ToListAsync();

// Debug
var (aql, bindVars) = q.ToAql();

Let clauses for subqueries

Note: database/transaction is only specified on the root expression, not on the inner one

var q = from p in Arango.Query<Project>("test")
let clients = (from c in Arango.Query<Client>() select c.Key)
select new {p.Name, Clients = Aql.As<string[]>(clients) };

await q.ToListAsync();

Update

var q = Arango.Query<Project>("test")
  .Where(x => x.Name == "Project A")
  .Update(x => new
  {
    Name = Aql.Concat(x.Name, "2")
  }, x => x.Key);
		
await q.ToListAsync();

Partial Update

  • Note: To push an object to inner collection.
var q = Arango.Query<Project>("test")
  .PartialUpdate(p=>p.hobbies, x => new
  {
    "val1"=1,"val2"=2
  }, x => x.Key);
		
await q.ToListAsync();

will be converted to:

FOR doc IN Project
UPDATE doc WITH {
  hobbies: PUSH(doc.hobbies, {"val1": 1, "val2":2})
} IN users

Remove

await Arango.Query<Project>("test")
.Where(x => x.Name == "Project A")
.Remove().In<Project>().Select(x => x.Key).ToListAsync();

OPTIONS

"Option" is the query option that exists in ArangoDb. Some operations like FOR / Graph Traversal / SEARCH / COLLECT / INSERT / UPDATE / REPLACE / UPSERT / REMOVE would support "Options".

await Arango.Query<Project>("test")
.Where(x => x.Name == "Project A")
.Options(() => new { indexHint = "byName" }).ToListAsync();

Create index

await arango.Index.CreateAsync("database", "collection", new ArangoIndex
{
    Fields = new List<string> {"SomeValue"},
    Type = ArangoIndexType.Hash
});

Create analyzer

await arango.Analyzer.CreateAsync("database", new ArangoAnalyzer
{
    Name = "text_de_nostem",
    Type = "text",
    Properties = new ArangoAnalyzerProperties
    {
        Locale = "de.utf-8",
        Case = ArangoAnalyzerCase.Lower,
        Accent = false,
        Stopwords = new List<string>(),
        Stemming = false
    },
    Features = new List<string> { "position", "norm", "frequency" }
});

Create view

await arango.View.CreateAsync("database", new ArangoView
{
    Name = "SomeView",
    Links = new Dictionary<string, ArangoLinkProperty>
    {
        ["collection"] = new ArangoLinkProperty
        {
            Fields = new Dictionary<string, ArangoLinkProperty>
            {
                ["SomeProperty"] = new ArangoLinkProperty
                {
                    Analyzers = new List<string>
                    {
                        "text_en"
                    }
                }
            }
        }
    },
    PrimarySort = new List<ArangoSort>
    {
        new ArangoSort
        {
            Field = "SomeProperty",
            Direction = ArangoSortDirection.Asc
        }
    }
});

Create graph

await arango.Collection.CreateAsync("database", "vertices", ArangoCollectionType.Document);
await Arango.Collection.CreateAsync("database", "edges", ArangoCollectionType.Edge);

await Arango.Graph.CreateAsync("database", new ArangoGraph
{
    Name = "graph",
    EdgeDefinitions = new List<ArangoEdgeDefinition>
    {
        new()
        {
          Collection = "edges",
          From = new List<string> {"vertices"},
          To = new List<string> {"vertices"}
        }
    }
});

Graph manipulation

await arango.Graph.Vertex.CreateAsync("database", "graph", "vertices", new
{
    Key = "alice",
    Name = "Alice"
});

await arango.Graph.Vertex.CreateAsync("database", "graph", "vertices", new
{
    Key = "bob",
    Name = "Bob"
});

await arango.Graph.Edge.CreateAsync("database", "graph", "edges", new
{
    Key = "ab",
    From = "vertices/alice",
    To = "vertices/bob",
    Label = "friend"
});

await arango.Graph.Edge.UpdateAsync("database", "graph", "edges", "ab", new
{
    Label = "foe"
});

await arango.Graph.Vertex.RemoveAsync("database", "graph", "vertices", "bob");

Create custom function

await arango.Function.CreateAsync("database", new ArangoFunctionDefinition
{
  Name = "CUSTOM::TIMES10",
  Code = "function (a) { return a * 10; }",
  IsDeterministic = true
});

Stream transactions

var transaction = await arango.Transaction.BeginAsync("database", new ArangoTransaction
{
    Collections = new ArangoTransactionScope
    {
        Write = new List<string> { "collection" }
    }
});

await arango.Document.CreateAsync(transaction, "collection", new
{
    Key = Guid.NewGuid(),
    SomeValue = 1
});

await arango.Document.CreateAsync(transaction, "collection", new
{
    Key = Guid.NewGuid(),
    SomeValue = 2
});

await arango.Transaction.CommitAsync(transaction);

Foxx services

// Build Foxx service zip archive
await using var ms = new MemoryStream();
using (var zip = new ZipArchive(ms, ZipArchiveMode.Create, true, Encoding.UTF8))
{
    await using (var manifest = zip.CreateEntry("manifest.json").Open())
    {
        await manifest.WriteAsync(Encoding.UTF8.GetBytes(@"
{
  ""$schema"": ""http://json.schemastore.org/foxx-manifest"",
  ""name"": ""SampleService"",
  ""description"": ""test"",
  ""version"": ""1.0.0"",
  ""license"": ""MIT"",
  ""engines"": {
    ""arangodb"": ""^3.0.0""
  },
  ""main"": ""index.js"",
  ""configuration"": {
    ""currency"": {
      ""description"": ""Currency symbol to use for prices in the shop."",
      ""default"": ""$"",
      ""type"": ""string""
    },
      ""secretKey"": {
      ""description"": ""Secret key to use for signing session tokens."",
      ""type"": ""password""
    }
  }
}
"));
    }

    await using (var readme = zip.CreateEntry("README").Open())
    {
        await readme.WriteAsync(Encoding.UTF8.GetBytes(@"
README!
"));
    }

    await using (var index = zip.CreateEntry("index.js").Open())
    {
        await index.WriteAsync(Encoding.UTF8.GetBytes(@"
'use strict';
const createRouter = require('@arangodb/foxx/router');
const router = createRouter();

module.context.use(router);
router.get('/hello-world', function (req, res) {{
  res.send({{ hello: 'world' }});
}})
.response(['application/json'], 'A generic greeting.')
.summary('Generic greeting')
.description('Prints a generic greeting.');
"));
    }
}

ms.Position = 0;

// install service
await Arango.Foxx.InstallServiceAsync("database", "/sample/service", ArangoFoxxSource.FromZip(ms));

// list services excluding system services
var services = await Arango.Foxx.ListServicesAsync("database", true);

// call service
var res = await Arango.Foxx.GetAsync<Dictionary<string, string>>("database", "/sample/service/hello-world");
Assert.Equal("world", res["hello"]);

Hot Backup (Enterprise Edition only)

var backup = await Arango.Backup.CreateAsync(new ArangoBackupRequest
{
	AllowInconsistent = false,
	Force = true,
	Label = "test",
	Timeout = 30
});

var backups = await Arango.Backup.ListAsync();

await Arango.Backup.RestoreAsync(backup.Id);

await Arango.Backup.DeleteAsync(backup.Id);