coronabytes / dotnet-arangodb

.NET Driver for ArangoDB
Apache License 2.0
67 stars 18 forks source link
arangodb arangodb-client csharp database driver graph linq netcore

Build Nuget Nuget

dotnet add package Core.Arango

.NET driver for ArangoDB

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

Serializer definition

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

Supported serializer:

Serialize DateTime / DateTimeOffset to Unix Timestamp

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

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

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);