Created
March 15, 2023 22:45
-
-
Save craigajohnson/a77a72835acfd867ac64247b59daa1f9 to your computer and use it in GitHub Desktop.
Dependency flow example (without DI container)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Root | |
{ | |
using Host; | |
internal class Program | |
{ | |
static async Task Main(string[] args) | |
{ | |
var host = new AppHost(); | |
var r = await host.InitializeAsync(args); | |
if (r.IsNotOK) // fail fast | |
{ | |
host.Log.LogError("Nope...", r.Message); | |
return; | |
} | |
host.Log.LogDebug("Host Initialized..."); | |
await host.StartAndWaitforShutdownAsync(); | |
host.Log.LogDebug("Host Stopped..."); | |
} | |
} | |
} | |
namespace Host | |
{ | |
using Client; | |
using Core; | |
using Infrastructure; | |
internal class AppHost : IAppHost | |
{ | |
public string RootFolder { get; private set; } | |
public AppConfig Config { get; private set; } | |
public ILogger Log { get; private set; } | |
public ILoggerFactory LogFactory { get; private set; } | |
public async Task<OpResult> InitializeAsync(string[] args) | |
{ | |
try | |
{ | |
RootFolder = GetType().Assembly.Folder(); | |
Config = JsonSerializer.Deserialize<AppConfig>(File.ReadAllText(Path.Combine(RootFolder, "AppSettings.json"))); | |
// add connection string, etc., from secrets provider, whatever is needed at this level of the initialization process | |
// Config.RootConnectionString = await Infrastructure.Secrets.GetSecretAsync(Config.TokenOfSomeSort, "ConnectionString") | |
// now that we have config, db info -> initialize host-level dependencies (e.g., logger, cache, telemetry provider, etc.) | |
await Task.Yield(); // perform any async initialization | |
return OpResult.OK; | |
} | |
catch (Exception ex) | |
{ | |
return ex; | |
} | |
} | |
public async Task StartAndWaitforShutdownAsync() | |
{ | |
await Task.Yield(); // do whatever is necessary when host starts | |
// wait on something here (e.g., GenericHost, etc.) | |
// pretend there's a new session... | |
var c = new ClientSession(); | |
await c.InitializeAsync(this); // wire-up occurs here | |
await c.Services.Auth.LoginAsync("grover", "cheese"); // graph is initialized | |
} | |
} | |
} | |
namespace Client | |
{ | |
using Core; | |
using Infrastructure; | |
public class ClientSession : IClientSession | |
{ | |
public IAppHost Host { get; private set; } | |
public Guid SessionID { get; private set; } | |
public ILogger Log { get; private set; } | |
public ServicesRoot Services { get; private set; } // | |
public bool IsAuthenticated => Services.Auth.IsAuthenticated; | |
// explicitly initialize dependencies - nothing magic | |
public async Task InitializeAsync(IAppHost host) | |
{ | |
Host = host; | |
SessionID = Guid.NewGuid(); | |
Log = Host.LogFactory.CreateLogger($"AppSession {SessionID}"); | |
Log.LogDebug("New Session Created", SessionID); | |
// initialize session dependencies (e.g., auth, data services, etc.) | |
Services = new(); | |
await Services.InitializeAsync(this); | |
// any others | |
} | |
public ValueTask DisposeAsync() | |
{ | |
Log.LogDebug("Session Disposed", SessionID); | |
return ValueTask.CompletedTask; | |
} | |
} | |
public class ServicesRoot // build out service hierarchy - can be both broad and deep - use interfaces if encapsulation is required | |
{ | |
private IClientSession Session; | |
public AuthServices Auth { get; private set; } | |
public AccountServices Account { get; private set; } | |
public async Task InitializeAsync(IClientSession session) | |
{ | |
Session = session; | |
// initialize EF context, etc. - note flow/availability of dependencies | |
// DBContext.Initialize(Session.Host.Config.RootConnectionString); | |
// you could do this via reflection or some other abstraction but it's probably unnecessary | |
Auth = new(session); | |
Account = new(session); | |
// add more nodes as needed... | |
await Task.Yield(); // perform any async initialization | |
} | |
public abstract class ServicesNode | |
{ | |
protected IClientSession Session; | |
public ServicesNode(IClientSession session) | |
{ | |
Session = session; | |
} | |
} | |
public class AuthServices : ServicesNode | |
{ | |
private AuthProvider Auth; | |
public AuthServices(IClientSession session) : base(session) | |
{ | |
Auth = new AuthProvider(session); // example of internal dependency - fronting via Services.Auth | |
} | |
public bool IsAuthenticated => Auth?.IsAuthenticated ?? false; | |
public async Task<OpResult> LoginAsync(string user, string pass) | |
{ | |
try | |
{ | |
return await Auth.AuthenticateAsync(user, pass).CAF(); | |
} | |
catch (Exception ex) | |
{ | |
return ex; | |
} | |
} | |
// add a bunch of services here... | |
} | |
public class AccountServices : ServicesNode | |
{ | |
public AccountServices(IClientSession session) : base(session) | |
{ | |
} | |
public Task<decimal> GetAccountBalanceAsync() | |
{ | |
// can invoke DB since we have access to a DBContext | |
return Task.FromResult(5.50m); | |
} | |
// add service calls here... | |
} | |
} | |
internal class AuthProvider : IAuthProvider | |
{ | |
private IClientSession Session; | |
private Guid? CurrentAuthToken; | |
internal AuthProvider(IClientSession session) | |
{ | |
Session = session; | |
} | |
public async Task<OpResult> AuthenticateAsync(string user, string password) | |
{ | |
await Task.Yield(); // perform auth... | |
Session.Log.LogDebug("Authenticating - {User}", user); | |
CurrentAuthToken = Guid.NewGuid(); | |
return OpResult.OK; | |
} | |
public bool IsAuthenticated => CurrentAuthToken.IsValid(); // add logic here | |
} | |
} | |
namespace Core | |
{ | |
using Infrastructure; | |
public class AppConfig | |
{ | |
public string HostName { get; set; } | |
public string RootConnectionString { get; set; } | |
// add config graph as needed... | |
} | |
public interface IAppHost | |
{ | |
public string RootFolder { get; } | |
public AppConfig Config { get; } | |
public ILogger Log { get; } | |
public ILoggerFactory LogFactory { get; } | |
// add as needed... | |
} | |
public interface IClientSession : IAsyncDisposable | |
{ | |
public ILogger Log { get; } | |
public IAppHost Host { get; } // if we're ok with everything, just include it. Otherwise create a new context | |
public Guid SessionID { get; } | |
// add as needed... | |
} | |
public interface IAuthProvider | |
{ | |
public Task<OpResult> AuthenticateAsync(string user, string password); | |
public bool IsAuthenticated { get; } | |
} | |
// add shared models as needed... | |
} | |
namespace Infrastructure // 3rd-party stuff, extensions, one-time initializers, etc. | |
{ | |
using System.Runtime.CompilerServices; | |
public readonly struct OpResult // example only | |
{ | |
public readonly OpStatus Status { get; init; } | |
public readonly string Message { get; init; } | |
public readonly Exception Exception { get; init; } | |
public OpResult() | |
{ | |
Status = OpStatus.OK; | |
Message = null; | |
Exception = null; | |
} | |
public bool IsOK => Status == OpStatus.OK; | |
public bool IsCancelled => Status == OpStatus.Cancelled; | |
public bool IsNotOK => !IsOK; | |
public static implicit operator OpResult(Exception ex) => new() { Status = OpStatus.Exception, Exception = ex }; | |
public static implicit operator Task<OpResult>(OpResult value) => Task.FromResult(value); | |
public static implicit operator ValueTask<OpResult>(OpResult value) => ValueTask.FromResult(value); | |
private static OpResult OKSingleton = new(); | |
public static ref OpResult OK => ref OKSingleton; | |
public static implicit operator OpResult(OpStatus status) | |
{ | |
if (status == OpStatus.OK) | |
{ | |
return OK; | |
} | |
return new() { Status = status }; | |
} | |
} | |
public enum OpStatus // example only | |
{ | |
OK, | |
Warning, | |
Failed, | |
Cancelled, | |
Exception | |
} | |
public static class Extensions // example only | |
{ | |
public static string Folder(this Assembly target) => Path.GetDirectoryName(new Uri(target.Location).LocalPath); | |
public static bool IsValid(this Guid g) => g != Guid.Empty; | |
public static bool IsNotValid(this Guid g) => !g.IsValid(); | |
public static bool IsValid(this Guid? g) => g.HasValue && g.Value != Guid.Empty; | |
public static bool IsNotValid(this Guid? g) => !g.IsValid(); | |
public static ConfiguredTaskAwaitable<TResult> CAF<TResult>(this Task<TResult> target) => target.ConfigureAwait(false); | |
public static ConfiguredTaskAwaitable CAF(this Task target) => target.ConfigureAwait(false); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment