Skip to content

Instantly share code, notes, and snippets.

@craigajohnson
Created March 15, 2023 22:45
Show Gist options
  • Save craigajohnson/a77a72835acfd867ac64247b59daa1f9 to your computer and use it in GitHub Desktop.
Save craigajohnson/a77a72835acfd867ac64247b59daa1f9 to your computer and use it in GitHub Desktop.
Dependency flow example (without DI container)
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