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

Information
Class: ZhEaIsNsAaBn.DbSeeder.Design.DesignTimeDiscovery
Assembly: ZhEaIsNsAaBn.DbSeeder.Design
File(s): /home/runner/work/ZhEaIsNsAaBn.Libraries.V1/ZhEaIsNsAaBn.Libraries.V1/src/DataBasePackages/ZhEaIsNsAaBn.DbSeeder.Design/DesignTime/DbSeederDesignTime.cs
Line coverage
75%
Covered lines: 25
Uncovered lines: 8
Coverable lines: 33
Total lines: 216
Line coverage: 75.7%
Branch coverage
96%
Covered branches: 27
Total branches: 28
Branch coverage: 96.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Factories(...)75%4480%
ServiceConfigurers(...)100%11100%
Select(...)100%1212100%
Create(...)100%210%
IsCreatable(...)100%1212100%
LoadableTypes(...)100%1166.66%

File(s)

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

#LineLine coverage
 1#region Usings
 2using System.Reflection;
 3using Microsoft.EntityFrameworkCore.Design;
 4using ZhEaIsNsAaBn.DbSeeder.Registrations;
 5#endregion
 6
 7namespace ZhEaIsNsAaBn.DbSeeder.Design;
 8
 9/// <summary>
 10/// The entry point of the <c>dotnet dbseeder</c> tool (it runs this inside the app's <c>deps.json</c> / <c>runtimeconfi
 11/// Finds the context, then runs the <see cref="DbSeederCli"/> commands on it:
 12/// <list type="number">
 13/// <item><c>IDesignTimeDbContextFactory&lt;TContext&gt;</c> classes in the startup and project assemblies create the co
 14/// <item>the seed registrations come from an <see cref="IDbSeederDesignTimeServices"/> class, else from the app's host 
 15/// runs until the host is built); without a factory the host also gives the context.</item>
 16/// </list>
 17/// More than one context: pick one with <c>--context</c> (short or full name).
 18/// </summary>
 19public static class DbSeederDesignTime
 20{
 21    /// <summary>Exit code when the tool can't start the command (no context, several, no seed registration, the host fa
 22    public const int ToolError = 2;
 23
 24    /// <summary>How long the app's entry point may take to build its host.</summary>
 25    public static readonly TimeSpan HostTimeout = TimeSpan.FromMinutes( 5 );
 26
 27
 28    /// <summary>Finds the context and runs one command on it, on the process console.</summary>
 29    /// <param name="startupAssembly">The startup project's assembly (its host and factories).</param>
 30    /// <param name="projectAssembly">The <c>--project</c> assembly (factories, design-time services); may be <paramref 
 31    /// <param name="projectDirectory">The project folder (<c>.dbseeder/</c>, migrations): the commands' <c>--dir</c> un
 32    /// <param name="contextName">The context's short or full type name; <see langword="null"/> when there is only one.<
 33    /// <param name="applicationArgs">Arguments for the app's entry point and for the factories' <c>CreateDbContext</c>.
 34    /// <param name="commandArgs">The DbSeeder command line, e.g. <c>[ "update", "--to", "20260301100000_Baseline" ]</c>
 35    /// <param name="cancellationToken">Token to cancel the command.</param>
 36    /// <returns>The command's exit code (0, 1, Spectre's for a bad command line) or <see cref="ToolError"/>.</returns>
 37    /// <exception cref="ArgumentNullException">An argument other than <paramref name="contextName"/> is <see langword="
 38    public static Task<int> RunAsync( Assembly startupAssembly, Assembly projectAssembly, string projectDirectory, strin
 39                                      string[] commandArgs, CancellationToken cancellationToken )
 40        => RunAsync( startupAssembly, projectAssembly, projectDirectory, contextName, applicationArgs, commandArgs, Ansi
 41
 42
 43    // errors: tool errors ("error: …"), plain and unwrapped like the tool's own (stderr in a real run).
 44    internal static async Task<int> RunAsync( Assembly startupAssembly, Assembly projectAssembly, string projectDirector
 45                                              string[] commandArgs, IAnsiConsole console, TextWriter errors, Cancellatio
 46    {
 47        ArgumentNullException.ThrowIfNull( errors );
 48
 49        ArgumentNullException.ThrowIfNull( startupAssembly );
 50        ArgumentNullException.ThrowIfNull( projectAssembly );
 51        ArgumentNullException.ThrowIfNull( projectDirectory );
 52        ArgumentNullException.ThrowIfNull( applicationArgs );
 53        ArgumentNullException.ThrowIfNull( commandArgs );
 54        ArgumentNullException.ThrowIfNull( console );
 55
 56        Assembly[] assemblies = startupAssembly == projectAssembly ? [ startupAssembly ] : [ startupAssembly, projectAss
 57        var        app        = startupAssembly.GetName().Name;
 58
 59        DesignTimeHost?  host     = null;
 60        ServiceProvider? services = null;
 61
 62        try
 63        {
 64            var factories   = DesignTimeDiscovery.Factories( assemblies );
 65            var configurers = DesignTimeDiscovery.ServiceConfigurers( assemblies );
 66
 67            if ( configurers.Count > 1 ) { return Fail( errors,$"More than one IDbSeederDesignTimeServices: {string.Join
 68
 69            IServiceProvider? provider;
 70
 71            if ( configurers.Count == 1 )
 72            {
 73                var collection = new ServiceCollection();
 74                ( (IDbSeederDesignTimeServices) Activator.CreateInstance( configurers[ 0 ] )! ).ConfigureServices( colle
 75                provider = services = collection.BuildServiceProvider();
 76            }
 77            else
 78            {
 79                try { host = await DesignTimeHost.BuildAsync( startupAssembly, applicationArgs, HostTimeout ).ConfigureA
 80                catch ( InvalidOperationException exception ) { return Fail( errors,$"The app's host could not be built:
 81
 82                provider = host?.Services;
 83            }
 84
 85
 86            var registrations = provider?.GetServices<SeedContextRegistration>().ToList() ?? [];
 87            var candidates    = factories.Keys.Concat( registrations.Select( r => r.ContextType ) ).Distinct().OrderBy( 
 88            var contextType   = DesignTimeDiscovery.Select( candidates, contextName, app, out var error );
 89
 90            if ( contextType is null ) { return Fail( errors,error! ); }
 91
 92            if ( provider is null || registrations.All( r => r.ContextType != contextType ) )
 93            {
 94                return Fail( errors,$"{contextType.Name} has no AddDbSeeder<{contextType.Name}>()…Build() registration (
 95            }
 96
 97
 98            DbContext? context = null;
 99
 100            if ( factories.TryGetValue( contextType, out var factory ) )
 101            {
 102                try { context = DesignTimeDiscovery.Create( factory, applicationArgs ); }
 103                catch ( InvalidOperationException exception ) { return Fail( errors,exception.Message ); }
 104            }
 105
 106            var args = WithDirectory( commandArgs, projectDirectory );
 107            var run     = typeof( DbSeederCli ).GetMethod( nameof( DbSeederCli.RunWithContextAsync ), BindingFlags.NonPu
 108
 109            return await ( (Task<int>) run.Invoke( null, [ args, provider, console, context, cancellationToken ] )! ).Co
 110        }
 111        finally
 112        {
 113            if ( services is not null ) { await services.DisposeAsync().ConfigureAwait( false ); }
 114
 115            if ( host is not null ) { await host.DisposeAsync().ConfigureAwait( false ); }
 116        }
 117    }
 118
 119
 120    /// The project folder as --dir, unless the command line names one (or there's no command: help, version).
 121    private static string[] WithDirectory( string[] commandArgs, string projectDirectory )
 122        => commandArgs.Length == 0 || commandArgs[ 0 ].StartsWith( '-' ) || commandArgs.Any( a => a is "-d" or "--dir" |
 123               ? commandArgs
 124               : [ ..commandArgs, "--dir", projectDirectory ];
 125
 126    private static int Fail( TextWriter errors, string message )
 127    {
 128        errors.WriteLine( $"error: {message}" );
 129
 130        return ToolError;
 131    }
 132}
 133
 134/// Finding factories, design-time services and the one context.
 135internal static class DesignTimeDiscovery
 136{
 137    /// Context type → its IDesignTimeDbContextFactory class (public, parameterless).
 138    public static Dictionary<Type, Type> Factories( IEnumerable<Assembly> assemblies )
 139    {
 3140        var factories = new Dictionary<Type, Type>();
 141
 558142        foreach ( var type in assemblies.SelectMany( LoadableTypes ).Where( IsCreatable ) )
 143        {
 552144            foreach ( var contract in type.GetInterfaces().Where( i => i.IsGenericType && i.GetGenericTypeDefinition() =
 145            {
 0146                factories.TryAdd( contract.GetGenericArguments()[ 0 ], type );
 147            }
 148        }
 149
 3150        return factories;
 151    }
 152
 153    public static List<Type> ServiceConfigurers( IEnumerable<Assembly> assemblies )
 3154        => assemblies.SelectMany( LoadableTypes ).Where( t => IsCreatable( t ) && typeof( IDbSeederDesignTimeServices ).
 155
 156    /// The context named <paramref name="name"/> (short or full), or the only one; <see langword="null"/> with the erro
 157    public static Type? Select( IReadOnlyList<Type> candidates, string? name, string? app, out string? error )
 158    {
 24159        error = null;
 160
 24161        if ( candidates.Count == 0 )
 162        {
 6163            error = $"No DbContext found in {app}. Add an IDesignTimeDbContextFactory<TContext> (with an IDbSeederDesign
 6164                  + "or register the context and AddDbSeeder<TContext>()…Build() in the app's host.";
 165
 6166            return null;
 167        }
 168
 18169        var found = string.Join( ", ", candidates.Select( t => t.Name ) );
 170
 18171        if ( name is null )
 172        {
 9173            if ( candidates.Count == 1 ) { return candidates[ 0 ]; }
 174
 3175            error = $"More than one DbContext found: {found}. Pick one with --context.";
 176
 3177            return null;
 178        }
 179
 15180        if ( candidates.FirstOrDefault( t => t.FullName == name ) is { } full ) { return full; }
 181
 9182        var matches = candidates.Where( t => t.Name == name ).ToList();
 183
 9184        switch ( matches.Count )
 185        {
 3186            case 1: return matches[ 0 ];
 6187            case 0: error = $"No DbContext named '{name}'. Found: {found}."; return null;
 188            default:
 3189                error = $"'{name}' matches {string.Join( " and ", matches.Select( t => t.FullName ) )}. Use the full nam
 190
 3191                return null;
 192        }
 193    }
 194
 195    public static DbContext Create( Type factory, string[] args )
 196    {
 0197        var instance = Activator.CreateInstance( factory )!;
 0198        var method   = factory.GetInterfaces().First( i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof( ID
 199
 0200        try { return (DbContext) method.Invoke( instance, [ args ] )!; }
 0201        catch ( TargetInvocationException exception ) when ( exception.InnerException is not null )
 202        {
 0203            throw new InvalidOperationException( $"{factory.Name}.CreateDbContext failed: {exception.InnerException.Mess
 204        }
 0205    }
 206
 207
 208    private static bool IsCreatable( Type type )
 4614209        => type is { IsClass: true, IsAbstract: false, ContainsGenericParameters: false } && ( type.IsPublic || type.IsN
 210
 211    private static IEnumerable<Type> LoadableTypes( Assembly assembly )
 212    {
 6213        try { return assembly.GetTypes(); }
 0214        catch ( ReflectionTypeLoadException exception ) { return exception.Types.Where( t => t is not null )!; }
 6215    }
 216}