< Summary

Information
Class: ArturRios.Data.Relational.Core.DependencyInjection.ServiceCollectionExtensions
Assembly: ArturRios.Data.Relational.Core
File(s): /home/ci/agent/workspace/dotnet-data-docs_main/app/src/ArturRios.Data.Relational.Core/DependencyInjection/ServiceCollectionExtensions.cs
Line coverage
92%
Covered lines: 66
Uncovered lines: 5
Coverable lines: 71
Total lines: 195
Line coverage: 92.9%
Branch coverage
75%
Covered branches: 21
Total branches: 28
Branch coverage: 75%
Method coverage
85%
Covered methods: 6
Fully covered methods: 4
Total methods: 7
Method coverage: 85.7%
Full method coverage: 57.1%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
EnsureProviderRegistered(...)100%44100%
MissingProvider(...)75%4491.66%
TryGetProviderType(...)66.66%66100%
ResolveProvider(...)50%2275%
AddDataConfigFromSettings(...)0%620%
AddDataConfig(...)100%22100%
AddDataConfigFromEnvironment(...)87.5%88100%

File(s)

/home/ci/agent/workspace/dotnet-data-docs_main/app/src/ArturRios.Data.Relational.Core/DependencyInjection/ServiceCollectionExtensions.cs

#LineLine coverage
 1using System.Reflection;
 2using ArturRios.Data.Relational.Core.Configuration;
 3using ArturRios.Data.Relational.Core.Exceptions;
 4using ArturRios.Data.Relational.Core.Interfaces;
 5using ArturRios.Data.Relational.Core.Providers;
 6using ArturRios.Data.Relational.Core.Repositories;
 7using ArturRios.Data.Relational.Core.Transactions;
 8using Microsoft.Extensions.Configuration;
 9using Microsoft.Extensions.DependencyInjection;
 10
 11namespace ArturRios.Data.Relational.Core.DependencyInjection;
 12
 13/// <summary>
 14///     Dependency-injection registration for the ArturRios.Data.Relational.Core relational stack.
 15/// </summary>
 16public static class ServiceCollectionExtensions
 17{
 18    // Best-effort eager check: only throws when it can prove no provider matches.
 19    // Factory-registered providers (services.AddSingleton<IDatabaseProvider>(sp => ...)) expose
 20    // neither ImplementationInstance nor ImplementationType, so TryGetProviderType cannot inspect
 21    // them. Their presence means absence cannot be proven, so validation defers to ResolveProvider
 22    // at first use instead of failing fast on a false negative.
 23    private static void EnsureProviderRegistered(IServiceCollection services, DatabaseType type)
 24    {
 1625        var providerTypes = services
 20026            .Where(d => d.ServiceType == typeof(IDatabaseProvider))
 1627            .Select(TryGetProviderType)
 1628            .ToList();
 29
 2430        if (providerTypes.Any(t => t == type))
 31        {
 432            return;
 33        }
 34
 1635        var hasUninspectableProvider = providerTypes.Any(t => t is null);
 36
 1237        if (hasUninspectableProvider)
 38        {
 439            return;
 40        }
 41
 842        throw MissingProvider(type);
 43    }
 44
 45    // Names the package and the registration call that actually exist for the type, so the message
 46    // can be followed as written (DatabaseType.SqLite is registered by AddSqliteProvider, for example).
 47    private static DataAccessException MissingProvider(DatabaseType type)
 48    {
 849        var (package, registration) = type switch
 850        {
 251            DatabaseType.PostgreSql => ("ArturRios.Data.PostgreSql", "AddPostgreSqlProvider"),
 252            DatabaseType.MySql => ("ArturRios.Data.MySql", "AddMySqlProvider"),
 453            DatabaseType.SqLite => ("ArturRios.Data.Sqlite", "AddSqliteProvider"),
 054            _ => ("the matching provider package", "its provider registration")
 855        };
 56
 857        return new DataAccessException(
 858        [
 859            $"No IDatabaseProvider registered for DatabaseType '{type}'. " +
 860            $"Install {package} and call services.{registration}()."
 861        ]);
 62    }
 63
 64    // Reads a registered provider's DatabaseType without building a ServiceProvider.
 65    // Providers are stateless with parameterless constructors, so instantiating to read Type is safe.
 66    private static DatabaseType? TryGetProviderType(ServiceDescriptor descriptor)
 67    {
 868        if (descriptor.ImplementationInstance is IDatabaseProvider instance)
 69        {
 470            return instance.Type;
 71        }
 72
 473        if (descriptor.ImplementationType is not { } implementationType)
 74        {
 275            return null;
 76        }
 77
 78        try
 79        {
 280            return Activator.CreateInstance(implementationType) is IDatabaseProvider created
 281                ? created.Type
 282                : null;
 83        }
 284        catch (Exception ex) when (ex is MissingMethodException or TargetInvocationException)
 85        {
 86            // A provider with constructor dependencies cannot be inspected here. Absence cannot be
 87            // proven, so defer to ResolveProvider at first use rather than failing registration.
 288            return null;
 89        }
 290    }
 91
 92    private static IDatabaseProvider ResolveProvider(
 93        IEnumerable<IDatabaseProvider> providers, DatabaseType type)
 94    {
 1295        var match = providers.FirstOrDefault(p => p.Type == type);
 696        if (match is null)
 97        {
 098            throw MissingProvider(type);
 99        }
 100
 6101        return match;
 102    }
 103
 104    /// <param name="services">The service collection.</param>
 105    extension(IServiceCollection services)
 106    {
 107        /// <summary>
 108        ///     Registers the configured <typeparamref name="TContext" />, repositories, and unit of work,
 109        ///     binding options from the given configuration section.
 110        /// </summary>
 111        /// <param name="configuration">Application configuration.</param>
 112        /// <param name="sectionName">Configuration section holding the options.</param>
 113        /// <typeparam name="TContext">The application's context type.</typeparam>
 114        public IServiceCollection AddDataConfigFromSettings<TContext>(IConfiguration configuration,
 115            string sectionName)
 116            where TContext : BaseDbContext
 117        {
 0118            var options = configuration.GetSection(sectionName).Get<BaseDbContextOptions>()
 0119                          ?? new BaseDbContextOptions();
 120
 0121            return services.AddDataConfig<TContext>(options);
 122        }
 123
 124        /// <summary>
 125        ///     Registers the configured <typeparamref name="TContext" />, repositories, and unit of work
 126        ///     from an explicit options instance.
 127        /// </summary>
 128        /// <param name="options">The database options.</param>
 129        /// <typeparam name="TContext">The application's context type.</typeparam>
 130        public IServiceCollection AddDataConfig<TContext>(BaseDbContextOptions options)
 131            where TContext : BaseDbContext
 132        {
 16133            services.AddDbContext<TContext>((sp, builder) =>
 16134            {
 6135                var provider = ResolveProvider(sp.GetServices<IDatabaseProvider>(), options.DatabaseType);
 6136                provider.Configure(builder, options.ConnectionString);
 22137            });
 138
 22139            services.AddScoped<BaseDbContext>(sp => sp.GetRequiredService<TContext>());
 140
 16141            services.AddScoped(typeof(IReadOnlyRepository<,>), typeof(EfRepository<,>));
 16142            services.AddScoped(typeof(IRepository<,>), typeof(EfRepository<,>));
 16143            services.AddScoped(typeof(IAsyncReadOnlyRepository<,>), typeof(EfRepository<,>));
 16144            services.AddScoped(typeof(IAsyncRepository<,>), typeof(EfRepository<,>));
 145
 16146            services.AddScoped<IUnitOfWork, EfUnitOfWork>();
 16147            services.AddScoped<IAsyncUnitOfWork, EfUnitOfWork>();
 148
 149            // Validate provider availability eagerly so misconfiguration fails at registration, not first use.
 16150            EnsureProviderRegistered(services, options.DatabaseType);
 151
 8152            return services;
 153        }
 154
 155        /// <summary>
 156        ///     Registers the configured <typeparamref name="TContext" />, repositories, and unit of work,
 157        ///     reading options entirely from environment variables. The appsettings section is not consulted.
 158        /// </summary>
 159        /// <param name="prefix">
 160        ///     Environment-variable name prefix. Variables are read as
 161        ///     <c>{prefix}_DATABASETYPE</c> and <c>{prefix}_CONNECTIONSTRING</c>.
 162        /// </param>
 163        /// <typeparam name="TContext">The application's context type.</typeparam>
 164        public IServiceCollection AddDataConfigFromEnvironment<TContext>(string prefix)
 165            where TContext : BaseDbContext
 166        {
 16167            if (string.IsNullOrWhiteSpace(prefix))
 168            {
 6169                throw new ArgumentException(
 6170                    "Environment-variable prefix must not be null or whitespace.", nameof(prefix));
 171            }
 172
 10173            var databaseTypeValue = Environment.GetEnvironmentVariable($"{prefix}_DATABASETYPE");
 174
 10175            if (!Enum.TryParse<DatabaseType>(databaseTypeValue, ignoreCase: true, out var databaseType) ||
 10176                !Enum.IsDefined(databaseType))
 177            {
 8178                throw new DataAccessException(
 8179                [
 8180                    $"Environment variable '{prefix}_DATABASETYPE' is unset or not a valid DatabaseType. " +
 8181                    $"Allowed values: {string.Join(", ", Enum.GetNames<DatabaseType>())}."
 8182                ]);
 183            }
 184
 2185            var connectionString =
 2186                Environment.GetEnvironmentVariable($"{prefix}_CONNECTIONSTRING") ?? string.Empty;
 187
 2188            return services.AddDataConfig<TContext>(new BaseDbContextOptions
 2189            {
 2190                DatabaseType = databaseType,
 2191                ConnectionString = connectionString
 2192            });
 193        }
 194    }
 195}