Skip to content

Restructure Project to Support DI and Nullity #36

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Nov 13, 2022
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@
[submodule "modules/functions-csharp"]
path = modules/functions-csharp
url = https://github.com/supabase-community/functions-csharp
[submodule "modules/core-csharp"]
path = modules/core-csharp
url = [email protected]:supabase-community/core-csharp.git
161 changes: 80 additions & 81 deletions Supabase/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,158 +29,152 @@ public enum ChannelEventType
All
}

public IGotrueClient<User, Session> Auth { get => AuthClient; }
public SupabaseFunctions Functions { get => new SupabaseFunctions(FunctionsClient, FunctionsUrl, GetAuthHeaders()); }

public IPostgrestClient Postgrest { get => PostgrestClient; }

public IRealtimeClient<Socket, Channel> Realtime { get => RealtimeClient; }

public IStorageClient<Bucket, FileObject> Storage { get => StorageClient; }

/// <summary>
/// Supabase Auth allows you to create and manage user sessions for access to data that is secured by access policies.
/// </summary>
public IGotrueClient<User, Session> AuthClient
public IGotrueClient<User, Session> Auth
{
get
{
return _authClient;
return _auth;
}
set
{
// Remove existing internal state listener (if applicable)
if (_authClient != null)
_authClient.StateChanged -= Auth_StateChanged;
if (_auth != null)
_auth.StateChanged -= Auth_StateChanged;

_authClient = value;
_authClient.StateChanged += Auth_StateChanged;
_auth = value;
_auth.StateChanged += Auth_StateChanged;
}
}
private IGotrueClient<User, Session> _authClient;
private IGotrueClient<User, Session> _auth;

/// <summary>
/// Supabase Realtime allows for realtime feedback on database changes.
/// </summary>
public IRealtimeClient<Socket, Channel> RealtimeClient
public IRealtimeClient<Socket, Channel> Realtime
{
get
{
return _realtimeClient;
return _realtime;
}
set
{
// Disconnect from previous socket (if applicable)
if (_realtimeClient != null)
_realtimeClient.Disconnect();
if (_realtime != null)
_realtime.Disconnect();

_realtimeClient = value;
_realtime = value;
}
}
private IRealtimeClient<Socket, Channel> _realtimeClient;
private IRealtimeClient<Socket, Channel> _realtime;

/// <summary>
/// Supabase Edge functions allow you to deploy and invoke edge functions.
/// </summary>
public IFunctionsClient FunctionsClient
public IFunctionsClient Functions
{
get => _functionsClient;
set => _functionsClient = value;
get => _functions;
set => _functions = value;
}
private IFunctionsClient _functionsClient;
private IFunctionsClient _functions;

/// <summary>
/// Supabase Postgrest allows for strongly typed REST interactions with the your database.
/// </summary>
public IPostgrestClient PostgrestClient
public IPostgrestClient Postgrest
{
get => _postgrestClient;
set => _postgrestClient = value;
get => _postgrest;
set => _postgrest = value;
}
private IPostgrestClient _postgrestClient;
private IPostgrestClient _postgrest;

/// <summary>
/// Supabase Storage allows you to manage user-generated content, such as photos or videos.
/// </summary>
public IStorageClient<Bucket, FileObject> StorageClient
public IStorageClient<Bucket, FileObject> Storage
{
get => _storageClient;
set => _storageClient = value;
get => _storage;
set => _storage = value;
}
private IStorageClient<Bucket, FileObject> _storageClient;

public string SupabaseKey { get; private set; }
public string SupabaseUrl { get; private set; }
public string AuthUrl { get; private set; }
public string RestUrl { get; private set; }
public string RealtimeUrl { get; private set; }
public string StorageUrl { get; private set; }
public string FunctionsUrl { get; private set; }
public string Schema { get; private set; }
private IStorageClient<Bucket, FileObject> _storage;

private string? supabaseKey;
private SupabaseOptions options;

public Client(string supabaseUrl, string supabaseKey, SupabaseOptions? options = null)
public Client(IGotrueClient<User, Session> auth, IRealtimeClient<Socket, Channel> realtime, IFunctionsClient functions, IPostgrestClient postgrest, IStorageClient<Bucket, FileObject> storage, string? supabaseKey, SupabaseOptions options)
{
_auth = auth;
_realtime = realtime;
_functions = functions;
_postgrest = postgrest;
_storage = storage;
this.supabaseKey = supabaseKey;
this.options = options;
}

SupabaseUrl = supabaseUrl;
SupabaseKey = supabaseKey;
public Client(string supabaseUrl, string? supabaseKey, SupabaseOptions? options = null)
{

this.supabaseKey = supabaseKey;

options ??= new SupabaseOptions();
this.options = options;

AuthUrl = string.Format(options.AuthUrlFormat, supabaseUrl);
RestUrl = string.Format(options.RestUrlFormat, supabaseUrl);
RealtimeUrl = string.Format(options.RealtimeUrlFormat, supabaseUrl).Replace("http", "ws");
StorageUrl = string.Format(options.StorageUrlFormat, supabaseUrl);
Schema = options.Schema;
var authUrl = string.Format(options.AuthUrlFormat, supabaseUrl);
var restUrl = string.Format(options.RestUrlFormat, supabaseUrl);
var realtimeUrl = string.Format(options.RealtimeUrlFormat, supabaseUrl).Replace("http", "ws");
var storageUrl = string.Format(options.StorageUrlFormat, supabaseUrl);
var schema = options.Schema;

// See: https://github.com/supabase/supabase-js/blob/09065a65f171bc28a9fd7b831af2c24e5f1a380b/src/SupabaseClient.ts#L77-L83
var isPlatform = new Regex(@"(supabase\.co)|(supabase\.in)").Match(supabaseUrl);

string? functionsUrl;
if (isPlatform.Success)
{
var parts = supabaseUrl.Split('.');
FunctionsUrl = $"{parts[0]}.functions.{parts[1]}.{parts[2]}";
functionsUrl = $"{parts[0]}.functions.{parts[1]}.{parts[2]}";
}
else
{
FunctionsUrl = string.Format(options.FunctionsUrlFormat, supabaseUrl);
functionsUrl = string.Format(options.FunctionsUrlFormat, supabaseUrl);
}

// Init Auth
var gotrueOptions = new Gotrue.ClientOptions
var gotrueOptions = new Gotrue.ClientOptions<Session>
{
Url = AuthUrl,
Headers = GetAuthHeaders(),
Url = authUrl,
AutoRefreshToken = options.AutoRefreshToken,
PersistSession = options.PersistSession,
SessionDestroyer = options.SessionDestroyer,
SessionPersistor = options.SessionPersistor,
SessionRetriever = options.SessionRetriever
SessionDestroyer = options.SessionHandler.SessionDestroyer,
SessionPersistor = options.SessionHandler.SessionPersistor,
SessionRetriever = options.SessionHandler.SessionRetriever<Session>
};

_authClient = new Gotrue.Client(gotrueOptions);
_authClient.StateChanged += Auth_StateChanged;
_auth = new Gotrue.Client(gotrueOptions);
_auth.StateChanged += Auth_StateChanged;
_auth.GetHeaders = () => GetAuthHeaders();


// Init Realtime

var realtimeOptions = new Realtime.ClientOptions
{
Parameters = { ApiKey = SupabaseKey }
Parameters = { ApiKey = this.supabaseKey }
};

_realtimeClient = new Realtime.Client(RealtimeUrl, realtimeOptions);
_realtime = new Realtime.Client(realtimeUrl, realtimeOptions);

_postgrestClient = new Postgrest.Client(RestUrl, new Postgrest.ClientOptions
{
Headers = GetAuthHeaders(),
Schema = Schema
});
_postgrest = new Postgrest.Client(restUrl, new Postgrest.ClientOptions { Schema = schema });
_postgrest.GetHeaders = () => GetAuthHeaders();

_functionsClient = new Functions.Client();
_functions = new Functions.Client(functionsUrl);
_functions.GetHeaders = () => GetAuthHeaders();

_storageClient = new Storage.Client(StorageUrl, GetAuthHeaders());
_storage = new Storage.Client(storageUrl, GetAuthHeaders());
_storage.GetHeaders = () => GetAuthHeaders();
}


Expand All @@ -193,11 +187,11 @@ public Client(string supabaseUrl, string supabaseKey, SupabaseOptions? options =
/// <returns></returns>
public async Task<ISupabaseClient<User, Session, Socket, Channel, Bucket, FileObject>> InitializeAsync()
{
await AuthClient.RetrieveSessionAsync();
await Auth.RetrieveSessionAsync();

if (options.AutoConnectRealtime)
{
await RealtimeClient.ConnectAsync();
await Realtime.ConnectAsync();
}
return this;
}
Expand All @@ -210,19 +204,19 @@ private void Auth_StateChanged(object sender, ClientStateChanged e)
// Ref: https://github.com/supabase-community/supabase-csharp/issues/12
case AuthState.SignedIn:
case AuthState.TokenRefreshed:
if (AuthClient.CurrentSession != null && AuthClient.CurrentSession.AccessToken != null)
if (Auth.CurrentSession?.AccessToken != null)
{
RealtimeClient.SetAuth(AuthClient.CurrentSession.AccessToken);
Realtime.SetAuth(Auth.CurrentSession.AccessToken);
}
_postgrestClient.Options.Headers = GetAuthHeaders();
_storageClient.Headers = GetAuthHeaders();
_postgrest.Options.Headers = GetAuthHeaders();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_postgrest already has GetHeaders set to call GetAuthHeaders() so I don't think this is necessary. It'll implicitly use the updated auth token for you so you can just remove these two lines.

_storage.Headers = GetAuthHeaders();
break;

// Remove Realtime Subscriptions on Auth Signout.
case AuthState.SignedOut:
foreach (var subscription in RealtimeClient.Subscriptions.Values)
foreach (var subscription in Realtime.Subscriptions.Values)
subscription.Unsubscribe();
RealtimeClient.Disconnect();
Realtime.Disconnect();
break;
}
}
Expand All @@ -240,22 +234,27 @@ private void Auth_StateChanged(object sender, ClientStateChanged e)
/// <param name="procedureName"></param>
/// <param name="parameters"></param>
/// <returns></returns>
public Task<BaseResponse> Rpc(string procedureName, Dictionary<string, object> parameters) => Postgrest.Rpc(procedureName, parameters);
public Task<BaseResponse> Rpc(string procedureName, Dictionary<string, object> parameters) => _postgrest.Rpc(procedureName, parameters);

internal Dictionary<string, string> GetAuthHeaders()
{
var headers = new Dictionary<string, string>();
headers["apiKey"] = SupabaseKey;

headers["X-Client-Info"] = Util.GetAssemblyVersion();

if (supabaseKey != null)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just spitballing here... would it be crazy to move supabaseKey into the Options object.

{
headers["apiKey"] = supabaseKey;
}

// In Regard To: https://github.com/supabase/supabase-csharp/issues/5
if (options.Headers.ContainsKey("Authorization"))
{
headers["Authorization"] = options.Headers["Authorization"];
}
else
{
var bearer = AuthClient?.CurrentSession?.AccessToken != null ? AuthClient.CurrentSession.AccessToken : SupabaseKey;
var bearer = Auth.CurrentSession?.AccessToken != null ? Auth.CurrentSession.AccessToken : supabaseKey;
headers["Authorization"] = $"Bearer {bearer}";
}

Expand Down
17 changes: 17 additions & 0 deletions Supabase/DefaultSupabaseSessionHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Threading.Tasks;
using Supabase.Gotrue;
using Supabase.Interfaces;

namespace Supabase
{
public class DefaultSupabaseSessionHandler : ISupabaseSessionHandler
{
public Task<bool> SessionPersistor<TSession>(TSession session) where TSession : Session => Task.FromResult(true);


public Task<TSession?> SessionRetriever<TSession>() where TSession : Session => Task.FromResult<TSession?>(null);


public Task<bool> SessionDestroyer() => Task.FromResult(true);
}
}
35 changes: 11 additions & 24 deletions Supabase/Interfaces/ISupabaseClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,18 @@
namespace Supabase.Interfaces
{
public interface ISupabaseClient<TUser, TSession, TSocket, TChannel, TBucket, TFileObject>
where TUser: User
where TSession: Session
where TSocket: IRealtimeSocket
where TChannel: IRealtimeChannel
where TBucket: Bucket
where TFileObject: FileObject
where TUser : User
where TSession : Session
where TSocket : IRealtimeSocket
where TChannel : IRealtimeChannel
where TBucket : Bucket
where TFileObject : FileObject
{
IGotrueClient<TUser, TSession> Auth { get; }
IGotrueClient<TUser, TSession> AuthClient { get; set; }
string AuthUrl { get; }
SupabaseFunctions Functions { get; }
IFunctionsClient FunctionsClient { get; set; }
string FunctionsUrl { get; }
IPostgrestClient Postgrest { get; }
IPostgrestClient PostgrestClient { get; set; }
IRealtimeClient<TSocket, TChannel> Realtime { get; }
IRealtimeClient<TSocket, TChannel> RealtimeClient { get; set; }
string RealtimeUrl { get; }
string RestUrl { get; }
string Schema { get; }
IStorageClient<TBucket, TFileObject> Storage { get; }
IStorageClient<TBucket, TFileObject> StorageClient { get; set; }
string StorageUrl { get; }
string SupabaseKey { get; }
string SupabaseUrl { get; }
IGotrueClient<TUser, TSession> Auth { get; set; }
IFunctionsClient Functions { get; set; }
IPostgrestClient Postgrest { get; set; }
IRealtimeClient<TSocket, TChannel> Realtime { get; set; }
IStorageClient<TBucket, TFileObject> Storage { get; set; }

ISupabaseTable<TModel, TChannel> From<TModel>() where TModel : BaseModel, new();
Task<ISupabaseClient<TUser, TSession, TSocket, TChannel, TBucket, TFileObject>> InitializeAsync();
Expand Down
26 changes: 26 additions & 0 deletions Supabase/Interfaces/ISupabaseSessionHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Supabase.Gotrue;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;

namespace Supabase.Interfaces
{
public interface ISupabaseSessionHandler
{
/// <summary>
/// Function called to persist the session (probably on a filesystem or cookie)
/// </summary>
Task<bool> SessionPersistor<TSession>(TSession session) where TSession : Session;

/// <summary>
/// Function to retrieve a session (probably from the filesystem or cookie)
/// </summary>
Task<TSession?> SessionRetriever<TSession>() where TSession : Session;

/// <summary>
/// Function to destroy a session.
/// </summary>
Task<bool> SessionDestroyer();
}
}
Loading