Skip to content

Instantly share code, notes, and snippets.

@rkttu
Last active June 6, 2023 09:18
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save rkttu/c69d6d3cfba066d0ae88e7041a86af06 to your computer and use it in GitHub Desktop.
Save rkttu/c69d6d3cfba066d0ae88e7041a86af06 to your computer and use it in GitHub Desktop.
Dependency Injection + Cancellation Token + Apartment State Configuration Supported Startup Framework

Rich Startup Framework for .NET Apps

This sample Gist implements support for several features to enable these features.

  • Dependency Injection (with Microsoft's default DI container)
  • Task Parallels Library with the cancellation-token support (including the cancel key press)
  • Unhandled exception
  • Unwrapping the aggregate exception
  • Apartment state configuration (for Windows Forms apps)
  • Entry-point wide timeout

I developed this sample code on the .NET 6.0 SDK. You may need to refactor this code when you use this code on other .NET runtimes such as .NET Framework, Mono, or Unity.

Usage

using Microsoft.Extensions.DependencyInjection;

public sealed class Program : IProgram
{
    static void Main() => IProgram.Start<Program>();

    public async Task<int> MainAsync(
        IEnumerable<string> args,
        IServiceCollection services,
        IServiceProvider provider,
        CancellationToken cancellationToken)
    {
        await Console.Out.WriteLineAsync($"Hello, World!");
        return await Task.FromResult(0);
    }
}
using Microsoft.Extensions.DependencyInjection;
public interface IProgram
{
public const int DefaultExitCode = 0;
public const int TimeoutExitCode = 124;
private static TProgram DefaultProgramFactory<TProgram>(IEnumerable<string> _)
where TProgram : IProgram
{
// Configure Apartment State
if (Enum.TryParse(
Environment.GetEnvironmentVariable("DOTNET_APARTMENT_STATE", EnvironmentVariableTarget.Process),
true, out ApartmentState apartmentState))
{
if (!Thread.CurrentThread.TrySetApartmentState(apartmentState))
{
Console.Error.WriteLine($"Warning: Cannot set apartment state of main thread. ({apartmentState})");
}
}
var program = Activator.CreateInstance<TProgram>();
return program;
}
protected static int Start<TProgram>(
Func<IEnumerable<string>, TProgram>? factory = null)
where TProgram : IProgram
=> Start(Environment.GetCommandLineArgs().Skip(1), factory);
protected static int Start<TProgram>(
IEnumerable<string> args,
Func<IEnumerable<string>, TProgram>? factory = null)
where TProgram : IProgram
{
// Generate program instance
var program = (factory ?? DefaultProgramFactory<TProgram>).Invoke(args);
// Configure Services
var services = new ServiceCollection();
program.OnConfigureServices(args, services);
// Configure Service Provider Factory
var spOptions = new ServiceProviderOptions()
{
ValidateOnBuild = true,
ValidateScopes = true,
};
var spFactory = new DefaultServiceProviderFactory(spOptions);
// Configure Service Provider
var provider = spFactory.CreateServiceProvider(services);
program.OnConfigureProvider(args, services, provider);
// Configure Process Cancellation
var cts = new CancellationTokenSource();
Console.CancelKeyPress += (s, e) =>
{
program.OnCancelKeyPress(s, e, args, services, provider);
if (!e.Cancel) cts.Cancel();
};
AppDomain.CurrentDomain.UnhandledException += (s, e)
=> program.OnUnhandledExceptionThrown(s, e, args, services, provider);
// Run Async Main Method
var task = program.MainAsync(args, services, provider, cts.Token);
// Retrieve and Setup Exit Code
int exitCode = DefaultExitCode;
if (program.OnAwaitMainThread(task, cts.Token, args, services, provider))
{
exitCode = TimeoutExitCode;
program.OnTimeout(args, services, provider);
}
else
exitCode = task.Result;
program.OnTerminating(exitCode, args, services, provider);
Environment.ExitCode = exitCode;
return exitCode;
}
Task<int> MainAsync(
IEnumerable<string> args,
IServiceCollection services,
IServiceProvider provider,
CancellationToken cancellationToken);
void OnConfigureServices(
IEnumerable<string> args,
IServiceCollection services)
{ }
void OnConfigureProvider(
IEnumerable<string> args,
IServiceCollection services,
IServiceProvider provider)
{ }
bool OnAwaitMainThread(
Task<int> task, CancellationToken cancellationToken,
IEnumerable<string> args,
IServiceCollection services,
IServiceProvider provider)
=> task.Wait(Timeout.Infinite, cancellationToken);
void OnCancelKeyPress(
object? sender, ConsoleCancelEventArgs e,
IEnumerable<string> args,
IServiceCollection services,
IServiceProvider provider)
{ }
void OnUnhandledExceptionThrown(
object? sender, UnhandledExceptionEventArgs e,
IEnumerable<string> args,
IServiceCollection services,
IServiceProvider provider)
{ }
void OnTimeout(IEnumerable<string> args,
IServiceCollection services,
IServiceProvider provider)
{ }
void OnTerminating(
int? exitCode,
IEnumerable<string> args,
IServiceCollection services,
IServiceProvider provider)
{ }
}
using Microsoft.Extensions.DependencyInjection;
public sealed class Program : IProgram
{
static void Main() => IProgram.Start<Program>();
public async Task<int> MainAsync(
IEnumerable<string> args,
IServiceCollection services,
IServiceProvider provider,
CancellationToken cancellationToken)
{
await Console.Out.WriteLineAsync($"Hello, World!");
return await Task.FromResult(0);
}
void IProgram.OnConfigureServices(IEnumerable<string> args, IServiceCollection services)
{
Console.WriteLine($"Called {nameof(IProgram.OnConfigureServices)}");
}
void IProgram.OnConfigureProvider(IEnumerable<string> args, IServiceCollection services, IServiceProvider provider)
{
Console.WriteLine($"Called {nameof(IProgram.OnConfigureProvider)}");
}
}
using Microsoft.Extensions.DependencyInjection;
internal static class StartupExtensions
{
public static Exception? Unwrap(this UnhandledExceptionEventArgs e)
=> Unwrap(e.ExceptionObject as Exception);
public static Exception? Unwrap<TException>(this TException? exception)
where TException : Exception
{
if (exception == null)
return null;
Exception current = exception;
while (current.InnerException != null)
current = current.InnerException;
return current;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment