Surreal-Net / Surreal.Net

Database driver for SurrealDB available for REST and RPC sessions.
Apache License 2.0
57 stars 7 forks source link

Update Surreal.Net Dependency Injection to enable both Multi Tenanted and Multi User scenarios #79

Open Du-z opened 2 years ago

Du-z commented 2 years ago

Preamble

Currently Surreal.Net functions as a standard DB Driver where a single set of credentials is used for all connections to the database. However SurrealDB can do a lot more than your standard DB like generating and consuming access tokens. For this reason we must be able to isolate each users session.

Consideration

The Changes

Dependency Injection

Update the projects Dependency Injection to allow for the service and options lifetime to be configured when being defined.

EF core uses the ServiceLifetime Enum to define both the contextLifetime and optionsLifetime.

By setting the ServiceLifetime to scoped each request (context of ASP.net) will have it's own instance of the SurrealDB driver.

Some Considerations

The Rest and RPC drivers are currently setup to be consumed as it they are singletons and are not lightweight enough to be created and destroyed repeatedly.

Fixing this should be as simple as setting the Drivers to use DI for HttpClient and WsClient (See typed clients).

Config will need to be injected also.

Driver Changes

Don't use default headers for the HttpClient, set them on a per request basis based on what is in the supplied config. (Is there an equivalent for WsClient?)

Middleware

Create a simple bit of middleware to extract a JWT from an incoming request and insert it into the Surreal.Net configuration for the driver to use during that request.

ProphetLamb commented 2 years ago

Right, so I've been mediating on the best practices to implement this. The current IDatabase implementations are about as heavy as a HttpClient, so we should use a pool for injecting a IDatabase, (on that note, the interface should really be called ISurrealClient to avoid confusion):

services.AddSingelton<ISurrealClientPool, SurrealClientPool>();
services.AddScoped<ISurrealClientHandle>(s => s.GetSingelton<ISurrealClientPool>().Rent()); // obtains a stale client from the pool, or create a new client 

public sealed class SurrealClientHandle : ISurrealClient {
  private readonly ISurrealClientPool _pool;

  public ISurrealClient Client { get; } \\ gets the client instance, if not disposed

\\ Proxy for client implementation 

  void Dispose(); \\ returns the client to the pool, closes the connection, doesn't dispose the client

}

We expect the ISurrealClient to be injected into a context object, which defined a strong typed API for a repository to interact with (Here we copy from the MongoDB driver/orm).

public sealed class SurveyContext : IDisposable {
  private readonly ISurrealClient _survey;
  private readonly ISurreadClient _user;

  .ctor(ISurrealClient survey, ISurrealClient user, IOptions<SurveySettings> surveySettings, IOptions<UsersSettings> userSettingd) {
    survey.SetConfig(Config.Create()[...]);
    user.SetConfig(Config.Create()[...])
  }
}
ProphetLamb commented 2 years ago

For this purpose we reduce the featureset of ISurrealClient ONLY to allow for query, signin, authenticate. Signup, Change, Update and the other methods are just aliases for queries and do not require additional client sided functionality.

The interface will also return a IAsyncEnumerable<T> using DeserializeAsyncEnumerable wrapping the networkstream instead of evaluating the entire response preemptively.

ProphetLamb commented 2 years ago

The reduction in functionality of the ISurrealClient interface is groundwork for implementing an ORM, to interact with the database. Here I plan on copying the approach of the MongoDb driver for basic queries.

ProphetLamb commented 2 years ago

@Du-z Anything to add, before I start hacking it together?

Du-z commented 2 years ago

In general I think stuff needs to be renamed to reduce ambiguity. Config is probably the most obvious one that needs a better name.

I am always an advocate and go out of my way to make using a service as simple as possible. To that end i would say we should move the pooling down a layer of abstraction so the end user doesn't even have to know about the pooling if they don't want to know about it.

It's not clear to me how DatabaseRpc and DatabaseRest would be heavy if they get their respective WsClient and HttpClient injected along with their Config.

DatabaseRest would be able to reuse the exact same HttpClient (even simultaneously). As long as we set the headers in the request using the config rather than the DefaultRequestHeaders. I believe we are using the thread safe methods already https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient?view=net-6.0#thread-safety.

DatabaseRpc would probably need to use a pool for WsClient.

Ultimately either way works for me though. If you can work in the ServiceLifetime concept that would be great for those that can simply use a singleton IDatabase