< Summary - ZhEaIsNsAaBn.Libraries coverage (affected: 23 packages)

Information
Class: ZhEaIsNsAaBn.DbSeeder.Design.DesignTimeHost
Assembly: ZhEaIsNsAaBn.DbSeeder.Design
File(s): /home/runner/work/ZhEaIsNsAaBn.Libraries.V1/ZhEaIsNsAaBn.Libraries.V1/src/DataBasePackages/ZhEaIsNsAaBn.DbSeeder.Design/DesignTime/DesignTimeHost.cs
Line coverage
3%
Covered lines: 2
Uncovered lines: 52
Coverable lines: 54
Total lines: 151
Line coverage: 3.7%
Branch coverage
3%
Covered branches: 1
Total branches: 30
Branch coverage: 3.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
BuildAsync()8.33%131126.25%
DisposeAsync()0%2040%
Abort()0%2040%
IsAbort(...)0%2040%
.ctor(...)100%210%
OnNext(...)0%620%
OnCompleted()100%210%
OnError(...)100%210%
.ctor(...)100%210%
OnNext(...)0%2040%
OnCompleted()100%210%
OnError(...)100%210%

File(s)

/home/runner/work/ZhEaIsNsAaBn.Libraries.V1/ZhEaIsNsAaBn.Libraries.V1/src/DataBasePackages/ZhEaIsNsAaBn.DbSeeder.Design/DesignTime/DesignTimeHost.cs

#LineLine coverage
 1#region Usings
 2using System.Diagnostics;
 3using System.Reflection;
 4#endregion
 5
 6namespace ZhEaIsNsAaBn.DbSeeder.Design;
 7
 8/// <summary>
 9/// The app's host, the way <c>dotnet ef</c> gets it: run the entry point on its own thread, catch the host when
 10/// <c>Microsoft.Extensions.Hosting</c> reports <c>HostBuilt</c> (generic host, <c>Host.CreateApplicationBuilder</c>,
 11/// <c>WebApplication</c>), then abort the entry point so the app never starts. Hosted services don't run.
 12/// </summary>
 13/// <remarks>Microsoft's <c>HostFactoryResolver.Sources</c> isn't on nuget.org, so this is the same mechanism, written h
 14internal sealed class DesignTimeHost : IAsyncDisposable
 15{
 16    private const string HostingListener = "Microsoft.Extensions.Hosting";
 17    private const string HostBuiltEvent  = "HostBuilt";
 18
 19    private readonly object _host;
 20
 21
 022    private DesignTimeHost( object host, IServiceProvider services )
 23    {
 024        _host    = host;
 25        Services = services;
 026    }
 27
 28
 29    public IServiceProvider Services { get; }
 30
 31
 32    /// <summary>The host, <see langword="null"/> without an entry point or when the entry point ends without building o
 33    /// <exception cref="InvalidOperationException">The entry point failed, or no host was built within <paramref name="
 34    public static async Task<DesignTimeHost?> BuildAsync( Assembly assembly, string[] args, TimeSpan timeout )
 35    {
 636        if ( assembly.EntryPoint is not { } entryPoint ) { return null; }
 37
 38
 039        var built = new TaskCompletionSource<object?>( TaskCreationOptions.RunContinuationsAsynchronously );
 40
 041        using var subscription = DiagnosticListener.AllListeners.Subscribe( new ListenerObserver( built ) );
 42
 043        var thread = new Thread( () =>
 044        {
 045            try
 046            {
 047                entryPoint.Invoke( null, entryPoint.GetParameters().Length == 0 ? null : [ args ] );
 048                built.TrySetResult( null );   // ended without a host (or the host was already caught: first result wins
 049            }
 050            catch ( TargetInvocationException exception ) when ( built.Task.IsCompleted || IsAbort( exception.InnerExcep
 051            {
 052                // our own abort, after HostBuilt
 053            }
 054            catch ( TargetInvocationException exception ) { built.TrySetException( exception.InnerException ?? exception
 055        } )
 056        {
 057            IsBackground = true,
 058            Name         = "dbseeder: app entry point"
 059        };
 60
 061        thread.Start();
 62
 063        if ( await Task.WhenAny( built.Task, Task.Delay( timeout ) ).ConfigureAwait( false ) != built.Task )
 64        {
 065            throw new InvalidOperationException( $"The app built no host within {timeout.TotalSeconds:0} s." );
 66        }
 67
 68        object? host;
 69
 070        try { host = await built.Task.ConfigureAwait( false ); }
 071        catch ( Exception exception ) when ( exception is not OperationCanceledException )
 72        {
 073            throw new InvalidOperationException( exception.Message, exception );
 74        }
 75
 076        if ( host is null ) { return null; }
 77
 78
 079        var services = host.GetType().GetInterfaces().FirstOrDefault( i => i.FullName == "Microsoft.Extensions.Hosting.I
 080                           as IServiceProvider
 081                    ?? throw new InvalidOperationException( $"The host ({host.GetType().FullName}) has no services." );
 82
 083        return new DesignTimeHost( host, services );
 384    }
 85
 86    public async ValueTask DisposeAsync()
 87    {
 088        switch ( _host )
 89        {
 090            case IAsyncDisposable asyncDisposable: await asyncDisposable.DisposeAsync().ConfigureAwait( false ); break;
 091            case IDisposable disposable:           disposable.Dispose(); break;
 92        }
 093    }
 94
 95
 96    /// Hosting's own HostAbortedException when it's there (apps' catch-all logging skips it), else ours.
 97    private static Exception Abort()
 098        => Type.GetType( "Microsoft.Extensions.Hosting.HostAbortedException, Microsoft.Extensions.Hosting.Abstractions",
 099           && Activator.CreateInstance( type ) is Exception hosting
 0100               ? hosting
 0101               : new DesignTimeHostAbortedException();
 102
 103    private static bool IsAbort( Exception? exception )
 0104        => exception is DesignTimeHostAbortedException || exception?.GetType().FullName == "Microsoft.Extensions.Hosting
 105
 106
 107    private sealed class ListenerObserver : IObserver<DiagnosticListener>
 108    {
 109        private readonly TaskCompletionSource<object?> _built;
 110
 111
 0112        public ListenerObserver( TaskCompletionSource<object?> built ) => _built = built;
 113
 114
 115        public void OnNext( DiagnosticListener value )
 116        {
 0117            if ( value.Name == HostingListener ) { value.Subscribe( new EventObserver( _built ) ); }
 0118        }
 119
 0120        public void OnCompleted() { }
 121
 0122        public void OnError( Exception error ) { }
 123    }
 124
 125    private sealed class EventObserver : IObserver<KeyValuePair<string, object?>>
 126    {
 127        private readonly TaskCompletionSource<object?> _built;
 128
 129
 0130        public EventObserver( TaskCompletionSource<object?> built ) => _built = built;
 131
 132
 133        public void OnNext( KeyValuePair<string, object?> value )
 134        {
 0135            if ( value.Key != HostBuiltEvent || !_built.TrySetResult( value.Value ) ) { return; }
 136
 137            // Stop the entry point before it runs the app (thrown out of the app's Build()).
 0138            throw Abort();
 139        }
 140
 0141        public void OnCompleted() { }
 142
 0143        public void OnError( Exception error ) { }
 144    }
 145}
 146
 147/// Thrown out of the app's <c>Build()</c> when Hosting has no <c>HostAbortedException</c>.
 148internal sealed class DesignTimeHostAbortedException : Exception
 149{
 150    public DesignTimeHostAbortedException() : base( "dotnet dbseeder stopped the app after its host was built." ) { }
 151}