< Summary

Information
Class: ArturRios.Data.Relational.Core.DependencyInjection.ServiceCollectionExtensions
Assembly: ArturRios.Data.Relational.Core
File(s): /home/runner/work/dotnet-data/dotnet-data/src/ArturRios.Data.Relational.Core/DependencyInjection/ServiceCollectionExtensions.cs
Line coverage
86%
Covered lines: 60
Uncovered lines: 9
Coverable lines: 69
Total lines: 186
Line coverage: 86.9%
Branch coverage
75%
Covered branches: 18
Total branches: 24
Branch coverage: 75%
Method coverage
83%
Covered methods: 5
Fully covered methods: 4
Total methods: 6
Method coverage: 83.3%
Full method coverage: 66.6%

Metrics

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

File(s)

/home/runner/work/dotnet-data/dotnet-data/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    {
 1025        var providerTypes = services
 12826            .Where(d => d.ServiceType == typeof(IDatabaseProvider))
 1027            .Select(TryGetProviderType)
 1028            .ToList();
 29
 1830        if (providerTypes.Any(t => t == type))
 31        {
 432            return;
 33        }
 34
 1035        var hasUninspectableProvider = providerTypes.Any(t => t is null);
 36
 637        if (hasUninspectableProvider)
 38        {
 439            return;
 40        }
 41
 242        throw new DataAccessException(
 243        [
 244            $"No IDatabaseProvider registered for DatabaseType '{type}'. " +
 245            $"Install and register the matching provider package " +
 246            $"(e.g. ArturRios.Data.Core.{type}) by calling its Add{type}Provider() extension."
 247        ]);
 48    }
 49
 50    // Reads a registered provider's DatabaseType without building a ServiceProvider.
 51    // Providers are stateless with parameterless constructors, so instantiating to read Type is safe.
 52    private static DatabaseType? TryGetProviderType(ServiceDescriptor descriptor)
 53    {
 854        if (descriptor.ImplementationInstance is IDatabaseProvider instance)
 55        {
 456            return instance.Type;
 57        }
 58
 459        if (descriptor.ImplementationType is not { } implementationType)
 60        {
 261            return null;
 62        }
 63
 64        try
 65        {
 266            return Activator.CreateInstance(implementationType) is IDatabaseProvider created
 267                ? created.Type
 268                : null;
 69        }
 270        catch (Exception ex) when (ex is MissingMethodException or TargetInvocationException)
 71        {
 72            // A provider with constructor dependencies cannot be inspected here. Absence cannot be
 73            // proven, so defer to ResolveProvider at first use rather than failing registration.
 274            return null;
 75        }
 276    }
 77
 78    private static IDatabaseProvider ResolveProvider(
 79        IEnumerable<IDatabaseProvider> providers, DatabaseType type)
 80    {
 1281        var match = providers.FirstOrDefault(p => p.Type == type);
 682        if (match is null)
 83        {
 084            throw new DataAccessException(
 085            [
 086                $"No IDatabaseProvider registered for DatabaseType '{type}'. " +
 087                $"Install and register the matching provider package " +
 088                $"(e.g. ArturRios.Data.Core.{type}) by calling its Add{type}Provider() extension."
 089            ]);
 90        }
 91
 692        return match;
 93    }
 94
 95    /// <param name="services">The service collection.</param>
 96    extension(IServiceCollection services)
 97    {
 98        /// <summary>
 99        ///     Registers the configured <typeparamref name="TContext" />, repositories, and unit of work,
 100        ///     binding options from the given configuration section.
 101        /// </summary>
 102        /// <param name="configuration">Application configuration.</param>
 103        /// <param name="sectionName">Configuration section holding the options.</param>
 104        /// <typeparam name="TContext">The application's context type.</typeparam>
 105        public IServiceCollection AddDataConfigFromSettings<TContext>(IConfiguration configuration,
 106            string sectionName)
 107            where TContext : BaseDbContext
 108        {
 0109            var options = configuration.GetSection(sectionName).Get<BaseDbContextOptions>()
 0110                          ?? new BaseDbContextOptions();
 111
 0112            return services.AddDataConfig<TContext>(options);
 113        }
 114
 115        /// <summary>
 116        ///     Registers the configured <typeparamref name="TContext" />, repositories, and unit of work
 117        ///     from an explicit options instance.
 118        /// </summary>
 119        /// <param name="options">The database options.</param>
 120        /// <typeparam name="TContext">The application's context type.</typeparam>
 121        public IServiceCollection AddDataConfig<TContext>(BaseDbContextOptions options)
 122            where TContext : BaseDbContext
 123        {
 10124            services.AddDbContext<TContext>((sp, builder) =>
 10125            {
 6126                var provider = ResolveProvider(sp.GetServices<IDatabaseProvider>(), options.DatabaseType);
 6127                provider.Configure(builder, options.ConnectionString);
 16128            });
 129
 16130            services.AddScoped<BaseDbContext>(sp => sp.GetRequiredService<TContext>());
 131
 10132            services.AddScoped(typeof(IReadOnlyRepository<>), typeof(EfRepository<>));
 10133            services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));
 10134            services.AddScoped(typeof(IAsyncReadOnlyRepository<>), typeof(EfRepository<>));
 10135            services.AddScoped(typeof(IAsyncRepository<>), typeof(EfRepository<>));
 136
 10137            services.AddScoped<IUnitOfWork, EfUnitOfWork>();
 10138            services.AddScoped<IAsyncUnitOfWork, EfUnitOfWork>();
 139
 140            // Validate provider availability eagerly so misconfiguration fails at registration, not first use.
 10141            EnsureProviderRegistered(services, options.DatabaseType);
 142
 8143            return services;
 144        }
 145
 146        /// <summary>
 147        ///     Registers the configured <typeparamref name="TContext" />, repositories, and unit of work,
 148        ///     reading options entirely from environment variables. The appsettings section is not consulted.
 149        /// </summary>
 150        /// <param name="prefix">
 151        ///     Environment-variable name prefix. Variables are read as
 152        ///     <c>{prefix}_DATABASETYPE</c> and <c>{prefix}_CONNECTIONSTRING</c>.
 153        /// </param>
 154        /// <typeparam name="TContext">The application's context type.</typeparam>
 155        public IServiceCollection AddDataConfigFromEnvironment<TContext>(string prefix)
 156            where TContext : BaseDbContext
 157        {
 16158            if (string.IsNullOrWhiteSpace(prefix))
 159            {
 6160                throw new ArgumentException(
 6161                    "Environment-variable prefix must not be null or whitespace.", nameof(prefix));
 162            }
 163
 10164            var databaseTypeValue = Environment.GetEnvironmentVariable($"{prefix}_DATABASETYPE");
 165
 10166            if (!Enum.TryParse<DatabaseType>(databaseTypeValue, ignoreCase: true, out var databaseType) ||
 10167                !Enum.IsDefined(databaseType))
 168            {
 8169                throw new DataAccessException(
 8170                [
 8171                    $"Environment variable '{prefix}_DATABASETYPE' is unset or not a valid DatabaseType. " +
 8172                    $"Allowed values: {string.Join(", ", Enum.GetNames<DatabaseType>())}."
 8173                ]);
 174            }
 175
 2176            var connectionString =
 2177                Environment.GetEnvironmentVariable($"{prefix}_CONNECTIONSTRING") ?? string.Empty;
 178
 2179            return services.AddDataConfig<TContext>(new BaseDbContextOptions
 2180            {
 2181                DatabaseType = databaseType,
 2182                ConnectionString = connectionString
 2183            });
 184        }
 185    }
 186}