Skip to content

Commit

Permalink
chore: basic PR cleanup
Browse files Browse the repository at this point in the history
Most of the changed files contain simple style, formatting, and naming
cleanup. Functional changes are mainly related to aligning this client
to the dgo client and are concentrated in these classes/interfaces:
 * DgraphCloudChannel: The static Create method implements the dgo
      DialCloud logic
 * DgraphClient: Users will interact with IDgraphClient interface instead
       of the DgraphClient class directly
 * Transaction: Added a Do method similar to the dgo implementation.
 * ITransaction: users will interact with this interface instead of the
        Transaction` class directly
 * IQuery: Represents a read-only transaction.
 * Transaction class is hidden behind this interface when creating it with
        client.NewReadOnlyTransaction
 * RequestBuilder & MutationBuilder

README.md is also heavily rewritten to match the dgo readme as closely
as possible.
  • Loading branch information
sachyco authored Jul 20, 2023
1 parent 0ef78e0 commit 09440c6
Show file tree
Hide file tree
Showing 22 changed files with 747 additions and 1,578 deletions.
433 changes: 319 additions & 114 deletions README.md

Large diffs are not rendered by default.

47 changes: 40 additions & 7 deletions source/Dgraph/Channels/CloudChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,63 @@

using Grpc.Core;
using Grpc.Net.Client;
using System.Threading.Tasks;

namespace Dgraph
{
public static class DgraphCloudChannel
{
private const string CloudPort = "443";

/// <summary>
/// Create a new TLS connection to a Dgraph Cloud backend.
/// </summary>
/// <returns>A new instance of <see cref="GrpcChannel"/></returns>
/// <exception cref="ArgumentException"></exception>
public static GrpcChannel Create(string address, string apiKey)
{
var credentials = CallCredentials.FromInterceptor((context, metadata) =>
if (string.IsNullOrWhiteSpace(address))
{
throw new ArgumentException($"Invalid address to Dgraph Cloud: {address}");
}
if (string.IsNullOrWhiteSpace(apiKey))
{
throw new ArgumentException("Invalid api key for Dgraph Cloud");
}

string grpcUri = address;
if (!grpcUri.StartsWith("http"))
{
grpcUri = $"https://{grpcUri}";
}
if (Uri.TryCreate(grpcUri, UriKind.Absolute, out Uri u) && u.Host.Contains("."))
{
if (!string.IsNullOrEmpty(apiKey))
if (u.Host.Contains(".grpc."))
{
metadata.Add("DG-Auth", apiKey);
grpcUri = $"https://{u.Host}:{CloudPort}";
}
else
{
var uriParts = u.Host.Split(".", 2);
grpcUri = $"https://{uriParts[0]}.grpc.{uriParts[1]}:{CloudPort}";
}
}
else
{
throw new ArgumentException($"Invalid address to Dgraph Cloud: {address}");
}

var credentials = CallCredentials.FromInterceptor((context, metadata) =>
{
metadata.Add("authorization", apiKey);
return Task.CompletedTask;
});

// SslCredentials is used here because this channel is using TLS.
// CallCredentials can't be used with ChannelCredentials.Insecure on non-TLS channels.
var channel = GrpcChannel.ForAddress(address, new GrpcChannelOptions
return GrpcChannel.ForAddress(grpcUri, new GrpcChannelOptions
{
Credentials = ChannelCredentials.Create(new SslCredentials(), credentials)
});
return channel;
}
}
}
}
147 changes: 57 additions & 90 deletions source/Dgraph/Client/DgraphClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,124 +14,110 @@
* limitations under the License.
*/

using System;
using System.Collections.Generic;
using Api;
using Dgraph.Api;
using Dgraph.Transactions;
using FluentResults;
using Grpc.Core;
using Grpc.Net.Client;
using System.Threading.Tasks;

// For unit testing. Allows to make mocks of the internal interfaces and factories
// so can test in isolation from a Dgraph instance.
//
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Dgraph.tests")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("DynamicProxyGenAssembly2")] // for NSubstitute

namespace Dgraph
{

public class DgraphClient : IDgraphClient, IDgraphClientInternal
{
public static IDgraphClient Create(params GrpcChannel[] channels)
{
return new DgraphClient(channels);
}

private readonly List<Api.Dgraph.DgraphClient> dgraphs =
new List<Api.Dgraph.DgraphClient>();
private readonly List<Api.Dgraph.DgraphClient> dgraphs;
private readonly GrpcChannel[] channels;

public DgraphClient(params GrpcChannel[] channels)
private DgraphClient(params GrpcChannel[] channels)
{
this.channels = channels;
this.dgraphs = new List<Api.Dgraph.DgraphClient>();
foreach (var chan in channels)
{
Api.Dgraph.DgraphClient client = new Api.Dgraph.DgraphClient(chan);
dgraphs.Add(client);
this.dgraphs.Add(new Api.Dgraph.DgraphClient(chan));
}
}

//
// ------------------------------------------------------
// Transactions
// ------------------------------------------------------
//
#region transactions

public ITransaction NewTransaction()
{
AssertNotDisposed();

return new Transaction(this);
}

public IQuery NewReadOnlyTransaction(Boolean bestEffort = false)
{
AssertNotDisposed();
#region IDgraphClient

return new ReadOnlyTransaction(this, bestEffort);
}

#endregion

//
// ------------------------------------------------------
// Execution
// ------------------------------------------------------
//
#region execution

private int NextConnection = 0;
private int GetNextConnection()
Task<Result> IDgraphClient.LoginIntoNamespace(string user, string password, ulong ns, CallOptions? options)
{
var next = NextConnection;
NextConnection = (next + 1) % dgraphs.Count;
return next;
return DgraphExecute(
async (dg) =>
{
await dg.LoginAsync(new LoginRequest
{
Userid = user,
Password = password,
Namespace = ns
}, options ?? new CallOptions());
return Result.Ok();
},
(rpcEx) => Result.Fail(new ExceptionalError(rpcEx))
);
}

public async Task<FluentResults.Result> Alter(Api.Operation op, CallOptions? options = null)
Task<Result> IDgraphClient.Alter(Api.Operation op, CallOptions? options)
{
return await DgraphExecute(
return DgraphExecute(
async (dg) =>
{
await dg.AlterAsync(op, options ?? new CallOptions());
return Results.Ok();
return Result.Ok();
},
(rpcEx) => Results.Fail(new FluentResults.ExceptionalError(rpcEx))
(rpcEx) => Result.Fail(new ExceptionalError(rpcEx))
);
}

public async Task<FluentResults.Result<string>> CheckVersion(CallOptions? options = null)
Task<Result<string>> IDgraphClient.CheckVersion(CallOptions? options)
{
return await DgraphExecute(
return DgraphExecute(
async (dg) =>
{
var versionResult = await dg.CheckVersionAsync(new Check(), options ?? new CallOptions());
return Results.Ok<string>(versionResult.Tag); ;
return Result.Ok<string>(versionResult.Tag); ;
},
(rpcEx) => Results.Fail<string>(new FluentResults.ExceptionalError(rpcEx))
(rpcEx) => Result.Fail<string>(new ExceptionalError(rpcEx))
);
}

public async Task<FluentResults.Result> Login(Api.LoginRequest lr, CallOptions? options = null)
ITransaction IDgraphClient.NewTransaction()
{
return await DgraphExecute(
async (dg) =>
{
await dg.LoginAsync(lr, options ?? new CallOptions());
return Results.Ok();
},
(rpcEx) => Results.Fail(new FluentResults.ExceptionalError(rpcEx))
);
AssertNotDisposed();
return new Transaction(client: this);
}

IQuery IDgraphClient.NewReadOnlyTransaction(bool bestEffort)
{
AssertNotDisposed();
return new Transaction(client: this, readOnly: true, bestEffort: bestEffort);
}

#endregion

#region execution

public async Task<T> DgraphExecute<T>(
Func<Api.Dgraph.DgraphClient, Task<T>> execute,
Func<RpcException, T> onFail
)
{

AssertNotDisposed();

try
{
return await execute(dgraphs[GetNextConnection()]);
// Randomly pick the next client to use.
var nextClient = dgraphs[Random.Shared.Next(dgraphs.Count)];
return await execute(nextClient);
}
catch (RpcException rpcEx)
{
Expand All @@ -141,26 +127,9 @@ Func<RpcException, T> onFail

#endregion

//
// ------------------------------------------------------
// Disposable Pattern
// ------------------------------------------------------
//
#region disposable pattern

// see disposable pattern at : https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/dispose-pattern
// and http://reedcopsey.com/tag/idisposable/
//
// Trying to follow the rules here
// https://blog.stephencleary.com/2009/08/second-rule-of-implementing-idisposable.html
// for all the dgraph dispose bits
//
// For this class, it has only managed IDisposable resources, so it just needs to call the Dispose()
// of those resources. It's safe to have nothing else, because IDisposable.Dispose() must be safe to call
// multiple times. Also don't need a finalizer. So this simplifies the general pattern, which isn't needed here.

bool disposed; // = false;
protected bool Disposed => disposed;
#region IDisposable

private bool Disposed = false;

protected void AssertNotDisposed()
{
Expand All @@ -179,16 +148,14 @@ protected virtual void DisposeIDisposables()
{
if (!Disposed)
{
this.disposed = true;
foreach (var dgraph in dgraphs)
this.Disposed = true;
foreach (var channel in this.channels)
{
// FIXME:
// can't get to the chans??
// dgraph. Dispose();
channel.Dispose();
}
}
}

#endregion
}
}
}
31 changes: 21 additions & 10 deletions source/Dgraph/Client/IDgraphClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,12 @@
* limitations under the License.
*/

using System;
using System.Threading.Tasks;
using Dgraph.Transactions;
using FluentResults;
using Grpc.Core;

namespace Dgraph
{

/// <summary>
/// An IDgraphClient is connected to a Dgraph cluster (to one or more Alpha
/// nodes). Once a client is made, the client manages the connections and
Expand All @@ -31,16 +29,30 @@ namespace Dgraph
/// has been disposed and calls are made.</exception>
public interface IDgraphClient : IDisposable
{
/// <summary>
/// Log in the client to the default namespace (0) using the provided
/// credentials. Valid for the duration the client is alive.
/// </summary>
Task<Result> Login(string user, string password, CallOptions? options = null)
{
return LoginIntoNamespace(user, password, 0, options);
}

/// <summary>
/// Log in the client to the provided namespace using the provided
/// credentials. Valid for the duration the client is alive.
/// </summary>
Task<Result> LoginIntoNamespace(string user, string password, ulong ns, CallOptions? options = null);

/// <summary>
/// Alter the Dgraph database (alter schema, drop everything, etc.).
/// </summary>
Task<FluentResults.Result> Alter(Api.Operation op, CallOptions? options = null);
Task<Result> Alter(Api.Operation op, CallOptions? options = null);

/// <summary>
/// Returns the Dgraph version string.
/// Create a transaction that can run queries and mutations.
/// </summary>
Task<FluentResults.Result<string>> CheckVersion(CallOptions? options = null);
ITransaction NewTransaction();

/// <summary>
/// Create a transaction that can only query.
Expand All @@ -56,9 +68,8 @@ public interface IDgraphClient : IDisposable
IQuery NewReadOnlyTransaction(Boolean bestEffort = false);

/// <summary>
/// Create a transaction that can run queries and mutations.
/// Returns the Dgraph version string.
/// </summary>
ITransaction NewTransaction();

Task<Result<string>> CheckVersion(CallOptions? options = null);
}
}
}
5 changes: 0 additions & 5 deletions source/Dgraph/Client/IDgraphClientInternal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,19 @@
* limitations under the License.
*/

using System;
using System.Threading.Tasks;
using Grpc.Core;

namespace Dgraph
{

/// <summary>
/// Internal dealings of clients with Dgraph --- Not part of the
/// external interface
/// </summary>
internal interface IDgraphClientInternal
{

Task<T> DgraphExecute<T>(
Func<Api.Dgraph.DgraphClient, Task<T>> execute,
Func<RpcException, T> onFail
);

}
}
Loading

0 comments on commit 09440c6

Please sign in to comment.