diff --git a/src/Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite/HostBuilderEntityFrameworkCoreExtensions.cs b/src/Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite/HostBuilderEntityFrameworkCoreExtensions.cs index 5d8da84e..70998d68 100644 --- a/src/Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite/HostBuilderEntityFrameworkCoreExtensions.cs +++ b/src/Extensions.Hosting.Identity.EntityFrameworkCore.Sqlite/HostBuilderEntityFrameworkCoreExtensions.cs @@ -2,6 +2,8 @@ // ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; + using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; @@ -24,6 +26,119 @@ public static class HostBuilderEntityFrameworkCoreExtensions /// The receiver instance. extension(IHostApplicationBuilder builder) { + /// Configures Entity Framework Core with SQLite and ASP.NET Core Identity using IHostApplicationBuilder. + /// This method provides integration with the modern IHostApplicationBuilder pattern and + /// sets up both Entity Framework Core and ASP.NET Core Identity with the specified user and role types. + /// The type of the Entity Framework Core DbContext to use for data access. + /// The type representing application users for ASP.NET Core Identity. + /// The type representing application roles for ASP.NET Core Identity. + /// The name of the connection string in the configuration. Cannot be null or whitespace. + /// The same IHostApplicationBuilder instance so that additional calls can be chained. + [SuppressMessage( + "Design", + "SST2307", + Justification = "Explicit Identity types preserve the public overload signatures released in version 4.1.0.")] + public IHostApplicationBuilder AddSqliteWithIdentity< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TContext, + TUser, + TRole>(string connectionStringName) + where TContext : DbContext + where TUser : class + where TRole : class => + builder.AddSqliteWithIdentity(connectionStringName, ServiceLifetime.Scoped); + + /// Configures SQLite and Identity with an explicit service lifetime. + /// The Entity Framework Core DbContext type. + /// The Identity user type. + /// The Identity role type. + /// The configured connection string name. + /// The lifetime with which to register the DbContext service. + /// The same IHostApplicationBuilder instance so that additional calls can be chained. + /// Thrown if connectionStringName is null or consists only of white-space characters. + /// Thrown if the specified connection string is not found in the configuration. + [SuppressMessage( + "Design", + "SST2307", + Justification = "Explicit Identity types preserve the public overload signatures released in version 4.1.0.")] + public IHostApplicationBuilder AddSqliteWithIdentity< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TContext, + TUser, + TRole>( + string connectionStringName, + ServiceLifetime serviceLifetime) + where TContext : DbContext + where TUser : class + where TRole : class + { + _ = builder ?? throw new ArgumentNullException(nameof(builder)); + + ArgumentException.ThrowIfNullOrWhiteSpace(connectionStringName); + + var conString = builder.Configuration.GetConnectionString(connectionStringName); + _ = builder.Services + .AddDbContext( + options => options.UseSqlite(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), + serviceLifetime, + serviceLifetime == ServiceLifetime.Singleton ? ServiceLifetime.Singleton : ServiceLifetime.Scoped) + .AddDefaultIdentity() + .AddRoles() + .AddEntityFrameworkStores(); + return builder; + } + + /// Configures Entity Framework Core with SQLite and ASP.NET Core Identity (user only) using IHostApplicationBuilder. + /// This method provides integration with the modern IHostApplicationBuilder pattern and + /// sets up both Entity Framework Core and ASP.NET Core Identity with the specified user type only. + /// The type of the Entity Framework Core DbContext to use for data access. + /// The type representing application users for ASP.NET Core Identity. + /// The name of the connection string in the configuration. Cannot be null or whitespace. + /// The same IHostApplicationBuilder instance so that additional calls can be chained. + [SuppressMessage( + "Design", + "SST2307", + Justification = "Explicit Identity types preserve the public overload signatures released in version 4.1.0.")] + public IHostApplicationBuilder AddSqliteWithIdentity< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TContext, + TUser>(string connectionStringName) + where TContext : DbContext + where TUser : class => + builder.AddSqliteWithIdentity(connectionStringName, ServiceLifetime.Scoped); + + /// Configures SQLite and user-only Identity with an explicit service lifetime. + /// The Entity Framework Core DbContext type. + /// The Identity user type. + /// The configured connection string name. + /// The lifetime with which to register the DbContext service. + /// The same IHostApplicationBuilder instance so that additional calls can be chained. + /// Thrown if connectionStringName is null or consists only of white-space characters. + /// Thrown if the specified connection string is not found in the configuration. + [SuppressMessage( + "Design", + "SST2307", + Justification = "Explicit Identity types preserve the public overload signatures released in version 4.1.0.")] + public IHostApplicationBuilder AddSqliteWithIdentity< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TContext, + TUser>( + string connectionStringName, + ServiceLifetime serviceLifetime) + where TContext : DbContext + where TUser : class + { + _ = builder ?? throw new ArgumentNullException(nameof(builder)); + + ArgumentException.ThrowIfNullOrWhiteSpace(connectionStringName); + + var conString = builder.Configuration.GetConnectionString(connectionStringName); + _ = builder.Services + .AddDbContext( + options => options.UseSqlite(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), + serviceLifetime, + serviceLifetime == ServiceLifetime.Singleton ? ServiceLifetime.Singleton : ServiceLifetime.Scoped) + .AddDefaultIdentity() + .AddEntityFrameworkStores(); + return builder; + } + /// Configures Entity Framework Core with SQLite using the IHostApplicationBuilder pattern. /// This method provides integration with the modern IHostApplicationBuilder pattern /// introduced in .NET 7+. It registers the DbContext with SQLite using the connection string @@ -220,6 +335,142 @@ public IHostBuilder UseWebHostServices( /// The receiver instance. extension(IServiceCollection services) { + /// Configures Entity Framework Core with a SQLite provider and sets up ASP.NET Core Identity using the specified context, user, and role types. + /// This method registers the DbContext with the SQLite provider and configures ASP.NET Core + /// Identity to use the specified user and role types with Entity Framework Core stores. It is typically called + /// during application startup to enable authentication and authorization using SQLite as the backing + /// store. + /// The type of the Entity Framework Core DbContext to use for data access. + /// The type representing application users for ASP.NET Core Identity. + /// The type representing application roles for ASP.NET Core Identity. + /// The web host builder context containing configuration and environment information. Cannot be null. + /// The name of the connection string in the configuration to use for the SQLite database. Cannot be null or + /// whitespace. + /// The same IServiceCollection instance so that additional calls can be chained. + [SuppressMessage( + "Design", + "SST2307", + Justification = "Explicit Identity types preserve the public overload signatures released in version 4.1.0.")] + public IServiceCollection UseEntityFrameworkCoreSqlite< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TContext, + TUser, + TRole>( + WebHostBuilderContext context, + string connectionStringName) + where TContext : DbContext + where TUser : class + where TRole : class => + services.UseEntityFrameworkCoreSqlite( + context, + connectionStringName, + ServiceLifetime.Scoped); + + /// Configures SQLite and Identity with an explicit service lifetime. + /// The Entity Framework Core DbContext type. + /// The Identity user type. + /// The Identity role type. + /// The web host builder context. + /// The configured connection string name. + /// The lifetime with which to register the DbContext service. + /// The same IServiceCollection instance so that additional calls can be chained. + /// Thrown if connectionStringName is null or consists only of white-space characters. + /// Thrown if the specified connection string is not found in the configuration. + [SuppressMessage( + "Design", + "SST2307", + Justification = "Explicit Identity types preserve the public overload signatures released in version 4.1.0.")] + public IServiceCollection UseEntityFrameworkCoreSqlite< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TContext, + TUser, + TRole>( + WebHostBuilderContext context, + string connectionStringName, + ServiceLifetime serviceLifetime) + where TContext : DbContext + where TUser : class + where TRole : class + { + _ = services ?? throw new ArgumentNullException(nameof(services)); + _ = context ?? throw new ArgumentNullException(nameof(context)); + + ArgumentException.ThrowIfNullOrWhiteSpace(connectionStringName); + + var conString = context.Configuration.GetConnectionString(connectionStringName); + _ = services + .AddDbContext( + options => options.UseSqlite(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), + serviceLifetime, + serviceLifetime == ServiceLifetime.Singleton ? ServiceLifetime.Singleton : ServiceLifetime.Scoped) + .AddDefaultIdentity() + .AddRoles() + .AddEntityFrameworkStores(); + return services; + } + + /// Configures Entity Framework Core with a SQLite provider and ASP.NET Core Identity using the specified DbContext and user type. + /// This method registers the specified DbContext with a SQLite provider and configures ASP.NET + /// Core Identity to use Entity Framework Core stores. It is typically called during application startup to enable + /// authentication and data access using SQLite. + /// The type of the DbContext to use for Entity Framework Core operations. + /// The type representing the user entity for ASP.NET Core Identity. + /// The WebHostBuilderContext containing application configuration and environment information. + /// The name of the connection string in the configuration to use for the SQLite database. Cannot be null or + /// whitespace. + /// The IServiceCollection instance configured for SQLite and Identity. + [SuppressMessage( + "Design", + "SST2307", + Justification = "Explicit Identity types preserve the public overload signatures released in version 4.1.0.")] + public IServiceCollection UseEntityFrameworkCoreSqlite< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TContext, + TUser>( + WebHostBuilderContext context, + string connectionStringName) + where TContext : DbContext + where TUser : class => + services.UseEntityFrameworkCoreSqlite( + context, + connectionStringName, + ServiceLifetime.Scoped); + + /// Configures SQLite and user-only Identity with an explicit service lifetime. + /// The Entity Framework Core DbContext type. + /// The Identity user type. + /// The web host builder context. + /// The configured connection string name. + /// The lifetime with which to register the DbContext service. + /// The IServiceCollection instance with Entity Framework Core and Identity services configured for SQLite. + /// Thrown if connectionStringName is null or consists only of white-space characters. + /// Thrown if the specified connection string is not found in the configuration. + [SuppressMessage( + "Design", + "SST2307", + Justification = "Explicit Identity types preserve the public overload signatures released in version 4.1.0.")] + public IServiceCollection UseEntityFrameworkCoreSqlite< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TContext, + TUser>( + WebHostBuilderContext context, + string connectionStringName, + ServiceLifetime serviceLifetime) + where TContext : DbContext + where TUser : class + { + _ = services ?? throw new ArgumentNullException(nameof(services)); + _ = context ?? throw new ArgumentNullException(nameof(context)); + + ArgumentException.ThrowIfNullOrWhiteSpace(connectionStringName); + + var conString = context.Configuration.GetConnectionString(connectionStringName); + _ = services + .AddDbContext( + options => options.UseSqlite(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), + serviceLifetime, + serviceLifetime == ServiceLifetime.Singleton ? ServiceLifetime.Singleton : ServiceLifetime.Scoped) + .AddDefaultIdentity() + .AddEntityFrameworkStores(); + return services; + } + /// Configures Entity Framework Core with a SQLite provider and caller-provided Identity services. /// This method registers the DbContext with the SQLite provider and configures ASP.NET Core /// Identity using the supplied callback. It is typically called during application startup to enable diff --git a/src/Extensions.Hosting.Identity.EntityFrameworkCore/HostBuilderEntityFrameworkCoreExtensions.cs b/src/Extensions.Hosting.Identity.EntityFrameworkCore/HostBuilderEntityFrameworkCoreExtensions.cs index cb393975..70bcac1c 100644 --- a/src/Extensions.Hosting.Identity.EntityFrameworkCore/HostBuilderEntityFrameworkCoreExtensions.cs +++ b/src/Extensions.Hosting.Identity.EntityFrameworkCore/HostBuilderEntityFrameworkCoreExtensions.cs @@ -2,6 +2,8 @@ // ReactiveUI Association Incorporated licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; + using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; @@ -66,6 +68,119 @@ public string GetRequiredConnectionString(string connectionStringName) /// The receiver instance. extension(IHostApplicationBuilder builder) { + /// Configures Entity Framework Core with SQL Server and ASP.NET Core Identity using IHostApplicationBuilder. + /// This method provides integration with the modern IHostApplicationBuilder pattern and + /// sets up both Entity Framework Core and ASP.NET Core Identity with the specified user and role types. + /// The type of the Entity Framework Core DbContext to use for data access. + /// The type representing application users for ASP.NET Core Identity. + /// The type representing application roles for ASP.NET Core Identity. + /// The name of the connection string in the configuration. Cannot be null or whitespace. + /// The same IHostApplicationBuilder instance so that additional calls can be chained. + [SuppressMessage( + "Design", + "SST2307", + Justification = "Explicit Identity types preserve the public overload signatures released in version 4.1.0.")] + public IHostApplicationBuilder AddSqlServerWithIdentity< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TContext, + TUser, + TRole>(string connectionStringName) + where TContext : DbContext + where TUser : class + where TRole : class => + builder.AddSqlServerWithIdentity(connectionStringName, ServiceLifetime.Scoped); + + /// Configures SQL Server and Identity with an explicit service lifetime. + /// The Entity Framework Core DbContext type. + /// The Identity user type. + /// The Identity role type. + /// The configured connection string name. + /// The lifetime with which to register the DbContext service. + /// The same IHostApplicationBuilder instance so that additional calls can be chained. + /// Thrown if connectionStringName is null or consists only of white-space characters. + /// Thrown if the specified connection string is not found in the configuration. + [SuppressMessage( + "Design", + "SST2307", + Justification = "Explicit Identity types preserve the public overload signatures released in version 4.1.0.")] + public IHostApplicationBuilder AddSqlServerWithIdentity< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TContext, + TUser, + TRole>( + string connectionStringName, + ServiceLifetime serviceLifetime) + where TContext : DbContext + where TUser : class + where TRole : class + { + _ = builder ?? throw new ArgumentNullException(nameof(builder)); + + ArgumentException.ThrowIfNullOrWhiteSpace(connectionStringName); + + var conString = builder.Configuration.GetConnectionString(connectionStringName); + _ = builder.Services + .AddDbContext( + options => options.UseSqlServer(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), + serviceLifetime, + serviceLifetime == ServiceLifetime.Singleton ? ServiceLifetime.Singleton : ServiceLifetime.Scoped) + .AddDefaultIdentity() + .AddRoles() + .AddEntityFrameworkStores(); + return builder; + } + + /// Configures Entity Framework Core with SQL Server and ASP.NET Core Identity (user only) using IHostApplicationBuilder. + /// This method provides integration with the modern IHostApplicationBuilder pattern and + /// sets up both Entity Framework Core and ASP.NET Core Identity with the specified user type only. + /// The type of the Entity Framework Core DbContext to use for data access. + /// The type representing application users for ASP.NET Core Identity. + /// The name of the connection string in the configuration. Cannot be null or whitespace. + /// The same IHostApplicationBuilder instance so that additional calls can be chained. + [SuppressMessage( + "Design", + "SST2307", + Justification = "Explicit Identity types preserve the public overload signatures released in version 4.1.0.")] + public IHostApplicationBuilder AddSqlServerWithIdentity< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TContext, + TUser>(string connectionStringName) + where TContext : DbContext + where TUser : class => + builder.AddSqlServerWithIdentity(connectionStringName, ServiceLifetime.Scoped); + + /// Configures SQL Server and user-only Identity with an explicit service lifetime. + /// The Entity Framework Core DbContext type. + /// The Identity user type. + /// The configured connection string name. + /// The lifetime with which to register the DbContext service. + /// The same IHostApplicationBuilder instance so that additional calls can be chained. + /// Thrown if connectionStringName is null or consists only of white-space characters. + /// Thrown if the specified connection string is not found in the configuration. + [SuppressMessage( + "Design", + "SST2307", + Justification = "Explicit Identity types preserve the public overload signatures released in version 4.1.0.")] + public IHostApplicationBuilder AddSqlServerWithIdentity< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TContext, + TUser>( + string connectionStringName, + ServiceLifetime serviceLifetime) + where TContext : DbContext + where TUser : class + { + _ = builder ?? throw new ArgumentNullException(nameof(builder)); + + ArgumentException.ThrowIfNullOrWhiteSpace(connectionStringName); + + var conString = builder.Configuration.GetConnectionString(connectionStringName); + _ = builder.Services + .AddDbContext( + options => options.UseSqlServer(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), + serviceLifetime, + serviceLifetime == ServiceLifetime.Singleton ? ServiceLifetime.Singleton : ServiceLifetime.Scoped) + .AddDefaultIdentity() + .AddEntityFrameworkStores(); + return builder; + } + /// Configures Entity Framework Core with SQL Server using the IHostApplicationBuilder pattern. /// This method provides integration with the modern IHostApplicationBuilder pattern /// introduced in .NET 7+. It registers the DbContext with SQL Server using the connection string @@ -261,6 +376,142 @@ public IHostBuilder UseWebHostServices( /// The receiver instance. extension(IServiceCollection services) { + /// Configures SQL Server and ASP.NET Core Identity with the specified context, user, and role types. + /// This method sets up Entity Framework Core to use SQL Server as the database provider and + /// configures ASP.NET Core Identity with the specified user and role types. It is typically called during + /// application startup to register data access and identity services. The method also adds the Entity Framework + /// stores for Identity, enabling user and role management backed by the specified DbContext. + /// The type of the Entity Framework Core DbContext to use for data access. + /// The type representing application users for ASP.NET Core Identity. Must be a reference type. + /// The type representing application roles for ASP.NET Core Identity. Must be a reference type. + /// The web host builder context containing configuration and environment information. Cannot be null. + /// The name of the connection string in the application's configuration to use for the SQL Server database. Cannot + /// be null or whitespace. + /// The same IServiceCollection instance so that additional calls can be chained. + [SuppressMessage( + "Design", + "SST2307", + Justification = "Explicit Identity types preserve the public overload signatures released in version 4.1.0.")] + public IServiceCollection UseEntityFrameworkCoreSqlServer< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TContext, + TUser, + TRole>( + WebHostBuilderContext context, + string connectionStringName) + where TContext : DbContext + where TUser : class + where TRole : class => + services.UseEntityFrameworkCoreSqlServer( + context, + connectionStringName, + ServiceLifetime.Scoped); + + /// Configures SQL Server and Identity with an explicit service lifetime. + /// The Entity Framework Core DbContext type. + /// The Identity user type. + /// The Identity role type. + /// The web host builder context. + /// The configured connection string name. + /// The lifetime with which to register the DbContext service. + /// The same IServiceCollection instance so that additional calls can be chained. + /// Thrown if connectionStringName is null or consists only of white-space characters. + /// Thrown if the specified connection string is not found in the configuration. + [SuppressMessage( + "Design", + "SST2307", + Justification = "Explicit Identity types preserve the public overload signatures released in version 4.1.0.")] + public IServiceCollection UseEntityFrameworkCoreSqlServer< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TContext, + TUser, + TRole>( + WebHostBuilderContext context, + string connectionStringName, + ServiceLifetime serviceLifetime) + where TContext : DbContext + where TUser : class + where TRole : class + { + _ = services ?? throw new ArgumentNullException(nameof(services)); + _ = context ?? throw new ArgumentNullException(nameof(context)); + + ArgumentException.ThrowIfNullOrWhiteSpace(connectionStringName); + + var conString = context.Configuration.GetConnectionString(connectionStringName); + _ = services + .AddDbContext( + options => options.UseSqlServer(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), + serviceLifetime, + serviceLifetime == ServiceLifetime.Singleton ? ServiceLifetime.Singleton : ServiceLifetime.Scoped) + .AddDefaultIdentity() + .AddRoles() + .AddEntityFrameworkStores(); + return services; + } + + /// Configures SQL Server with the specified DbContext and Identity user type. + /// This method adds the DbContext, ASP.NET Core Identity, and Entity Framework stores to the + /// service collection, enabling authentication and data access using SQL Server. The connection string must be + /// defined in the application's configuration under the provided name. + /// The type of the DbContext to register for use with SQL Server. + /// The type of the user entity to use with ASP.NET Core Identity. + /// The web host builder context containing configuration information. Cannot be null. + /// The name of the connection string in the configuration to use for the SQL Server database. Cannot be null or + /// whitespace. + /// The IServiceCollection instance configured for SQL Server and Identity. + [SuppressMessage( + "Design", + "SST2307", + Justification = "Explicit Identity types preserve the public overload signatures released in version 4.1.0.")] + public IServiceCollection UseEntityFrameworkCoreSqlServer< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TContext, + TUser>( + WebHostBuilderContext context, + string connectionStringName) + where TContext : DbContext + where TUser : class => + services.UseEntityFrameworkCoreSqlServer( + context, + connectionStringName, + ServiceLifetime.Scoped); + + /// Configures SQL Server and user-only Identity with an explicit service lifetime. + /// The Entity Framework Core DbContext type. + /// The Identity user type. + /// The web host builder context. + /// The configured connection string name. + /// The lifetime with which to register the DbContext service. + /// The IServiceCollection instance with Entity Framework Core and identity services configured for SQL Server. + /// Thrown if connectionStringName is null or consists only of white-space characters. + /// Thrown if the specified connection string is not found in the configuration. + [SuppressMessage( + "Design", + "SST2307", + Justification = "Explicit Identity types preserve the public overload signatures released in version 4.1.0.")] + public IServiceCollection UseEntityFrameworkCoreSqlServer< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TContext, + TUser>( + WebHostBuilderContext context, + string connectionStringName, + ServiceLifetime serviceLifetime) + where TContext : DbContext + where TUser : class + { + _ = services ?? throw new ArgumentNullException(nameof(services)); + _ = context ?? throw new ArgumentNullException(nameof(context)); + + ArgumentException.ThrowIfNullOrWhiteSpace(connectionStringName); + + var conString = context.Configuration.GetConnectionString(connectionStringName); + _ = services + .AddDbContext( + options => options.UseSqlServer(conString ?? throw new InvalidOperationException($"Connection string '{connectionStringName}' not found.")), + serviceLifetime, + serviceLifetime == ServiceLifetime.Singleton ? ServiceLifetime.Singleton : ServiceLifetime.Scoped) + .AddDefaultIdentity() + .AddEntityFrameworkStores(); + return services; + } + /// Configures SQL Server and ASP.NET Core Identity with caller-provided identity services. /// The type of the Entity Framework Core DbContext to use for data access. /// The web host builder context containing configuration and environment information. Cannot be null. diff --git a/src/tests/Extensions.Hosting.DataLogging.Tests/SqlServerIdentityCompatibilityTests.cs b/src/tests/Extensions.Hosting.DataLogging.Tests/SqlServerIdentityCompatibilityTests.cs new file mode 100644 index 00000000..9796f8bb --- /dev/null +++ b/src/tests/Extensions.Hosting.DataLogging.Tests/SqlServerIdentityCompatibilityTests.cs @@ -0,0 +1,145 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +extern alias SqlServer; +using SqlServerExtensions = SqlServer::ReactiveMarbles.Extensions.Hosting.Identity.EntityFrameworkCore.HostBuilderEntityFrameworkCoreExtensions; + +namespace Extensions.Hosting.DataLogging.Tests; + +/// Verifies the SQL Server Identity overloads shipped in version 4.1.0. +public sealed class SqlServerIdentityCompatibilityTests +{ + /// The configured connection string name. + private const string ConnectionStringName = "Identity"; + + /// The provider connection string used for registration. + private const string ConnectionString = "Server=(localdb)\\MSSQLLocalDB;Database=IdentityCompatibility;Trusted_Connection=True;"; + + /// Verifies application-builder overloads register custom Identity types and context lifetimes. + /// Whether to configure custom roles. + /// Whether to use the overload with the default lifetime. + /// The expected context lifetime. + /// A task representing the asynchronous test. + [Test] + [Arguments(false, true, ServiceLifetime.Scoped)] + [Arguments(true, true, ServiceLifetime.Scoped)] + [Arguments(false, false, ServiceLifetime.Scoped)] + [Arguments(true, false, ServiceLifetime.Scoped)] + [Arguments(false, false, ServiceLifetime.Singleton)] + [Arguments(true, false, ServiceLifetime.Singleton)] + [Arguments(false, false, ServiceLifetime.Transient)] + [Arguments(true, false, ServiceLifetime.Transient)] + public async Task AddSqlServerWithIdentity_CustomTypes_RegisterManagersAndLifetime(bool roles, bool defaultLifetime, ServiceLifetime lifetime) + { + var builder = Host.CreateApplicationBuilder(); + _ = builder.Configuration.AddInMemoryCollection( + [ + new KeyValuePair("ConnectionStrings:Identity", ConnectionString), + ]); + + var result = (roles, defaultLifetime) switch + { + (true, true) => SqlServerExtensions.AddSqlServerWithIdentity(builder, ConnectionStringName), + (true, false) => SqlServerExtensions.AddSqlServerWithIdentity(builder, ConnectionStringName, lifetime), + (false, true) => SqlServerExtensions.AddSqlServerWithIdentity(builder, ConnectionStringName), + (false, false) => SqlServerExtensions.AddSqlServerWithIdentity(builder, ConnectionStringName, lifetime), + }; + + await Assert.That(ReferenceEquals(result, builder)).IsTrue(); + await VerifyRegistrationAsync(builder.Services, roles, lifetime); + } + + /// Verifies service-collection overloads register custom Identity types and context lifetimes. + /// Whether to configure custom roles. + /// Whether to use the overload with the default lifetime. + /// The expected context lifetime. + /// A task representing the asynchronous test. + [Test] + [Arguments(false, true, ServiceLifetime.Scoped)] + [Arguments(true, true, ServiceLifetime.Scoped)] + [Arguments(false, false, ServiceLifetime.Scoped)] + [Arguments(true, false, ServiceLifetime.Scoped)] + [Arguments(false, false, ServiceLifetime.Singleton)] + [Arguments(true, false, ServiceLifetime.Singleton)] + [Arguments(false, false, ServiceLifetime.Transient)] + [Arguments(true, false, ServiceLifetime.Transient)] + public async Task UseEntityFrameworkCoreSqlServer_CustomTypes_RegisterManagersAndLifetime(bool roles, bool defaultLifetime, ServiceLifetime lifetime) + { + var configuration = new ConfigurationBuilder().AddInMemoryCollection( + [ + new KeyValuePair("ConnectionStrings:Identity", ConnectionString), + ]).Build(); + var context = new WebHostBuilderContext { Configuration = configuration }; + var services = new ServiceCollection(); + _ = services.AddLogging(); + var result = (roles, defaultLifetime) switch + { + (true, true) => SqlServerExtensions.UseEntityFrameworkCoreSqlServer(services, context, ConnectionStringName), + (true, false) => SqlServerExtensions.UseEntityFrameworkCoreSqlServer(services, context, ConnectionStringName, lifetime), + (false, true) => SqlServerExtensions.UseEntityFrameworkCoreSqlServer(services, context, ConnectionStringName), + (false, false) => SqlServerExtensions.UseEntityFrameworkCoreSqlServer(services, context, ConnectionStringName, lifetime), + }; + + await Assert.That(ReferenceEquals(result, services)).IsTrue(); + await VerifyRegistrationAsync(services, roles, lifetime); + } + + /// Verifies actual context, store, and manager resolution for the selected Identity types. + /// The configured services. + /// Whether custom role services are expected. + /// The expected context lifetime. + /// A task representing the asynchronous test. + private static async Task VerifyRegistrationAsync(IServiceCollection services, bool roles, ServiceLifetime lifetime) + { + var contextType = roles ? typeof(RoleContext) : typeof(UserContext); + ServiceDescriptor? contextDescriptor = null; + foreach (var descriptor in services) + { + if (descriptor.ServiceType == contextType) + { + contextDescriptor = descriptor; + break; + } + } + + await Assert.That(contextDescriptor).IsNotNull(); + await Assert.That(contextDescriptor!.Lifetime).IsEqualTo(lifetime); + await using var provider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); + using var firstScope = provider.CreateScope(); + using var secondScope = provider.CreateScope(); + var first = (DbContext)firstScope.ServiceProvider.GetRequiredService(contextType); + var repeated = (DbContext)firstScope.ServiceProvider.GetRequiredService(contextType); + var second = (DbContext)secondScope.ServiceProvider.GetRequiredService(contextType); + await Assert.That(first.Database.ProviderName).IsEqualTo("Microsoft.EntityFrameworkCore.SqlServer"); + await Assert.That(first.Database.GetDbConnection().DataSource).IsEqualTo(@"(localdb)\MSSQLLocalDB"); + await Assert.That(first.Database.GetDbConnection().Database).IsEqualTo("IdentityCompatibility"); + await Assert.That(ReferenceEquals(first, repeated)).IsEqualTo(lifetime != ServiceLifetime.Transient); + await Assert.That(ReferenceEquals(first, second)).IsEqualTo(lifetime == ServiceLifetime.Singleton); + await Assert.That(firstScope.ServiceProvider.GetRequiredService>()).IsNotNull(); + await Assert.That(firstScope.ServiceProvider.GetRequiredService>()).IsNotNull(); + if (roles) + { + await Assert.That(firstScope.ServiceProvider.GetRequiredService>()).IsNotNull(); + await Assert.That(firstScope.ServiceProvider.GetRequiredService>()).IsNotNull(); + } + else + { + await Assert.That(firstScope.ServiceProvider.GetService>()).IsNull(); + } + } + + /// Provides a custom application user. + public sealed class CustomUser : IdentityUser; + + /// Provides a custom application role. + public sealed class CustomRole : IdentityRole; + + /// Provides a context storing custom users without roles. + /// The configured context options. + public sealed class UserContext(DbContextOptions options) : IdentityUserContext(options); + + /// Provides a context storing custom users and roles. + /// The configured context options. + public sealed class RoleContext(DbContextOptions options) : IdentityDbContext(options); +} diff --git a/src/tests/Extensions.Hosting.DataLogging.Tests/SqliteIdentityCompatibilityTests.cs b/src/tests/Extensions.Hosting.DataLogging.Tests/SqliteIdentityCompatibilityTests.cs new file mode 100644 index 00000000..2acaa35f --- /dev/null +++ b/src/tests/Extensions.Hosting.DataLogging.Tests/SqliteIdentityCompatibilityTests.cs @@ -0,0 +1,144 @@ +// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved. +// ReactiveUI Association Incorporated licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +extern alias Sqlite; +using SqliteExtensions = Sqlite::ReactiveMarbles.Extensions.Hosting.Identity.EntityFrameworkCore.HostBuilderEntityFrameworkCoreExtensions; + +namespace Extensions.Hosting.DataLogging.Tests; + +/// Verifies the SQLite Identity overloads shipped in version 4.1.0. +public sealed class SqliteIdentityCompatibilityTests +{ + /// The configured connection string name. + private const string ConnectionStringName = "Identity"; + + /// The provider connection string used for registration. + private const string ConnectionString = "Data Source=:memory:"; + + /// Verifies application-builder overloads register custom Identity types and context lifetimes. + /// Whether to configure custom roles. + /// Whether to use the overload with the default lifetime. + /// The expected context lifetime. + /// A task representing the asynchronous test. + [Test] + [Arguments(false, true, ServiceLifetime.Scoped)] + [Arguments(true, true, ServiceLifetime.Scoped)] + [Arguments(false, false, ServiceLifetime.Scoped)] + [Arguments(true, false, ServiceLifetime.Scoped)] + [Arguments(false, false, ServiceLifetime.Singleton)] + [Arguments(true, false, ServiceLifetime.Singleton)] + [Arguments(false, false, ServiceLifetime.Transient)] + [Arguments(true, false, ServiceLifetime.Transient)] + public async Task AddSqliteWithIdentity_CustomTypes_RegisterManagersAndLifetime(bool roles, bool defaultLifetime, ServiceLifetime lifetime) + { + var builder = Host.CreateApplicationBuilder(); + _ = builder.Configuration.AddInMemoryCollection( + [ + new KeyValuePair("ConnectionStrings:Identity", ConnectionString), + ]); + + var result = (roles, defaultLifetime) switch + { + (true, true) => SqliteExtensions.AddSqliteWithIdentity(builder, ConnectionStringName), + (true, false) => SqliteExtensions.AddSqliteWithIdentity(builder, ConnectionStringName, lifetime), + (false, true) => SqliteExtensions.AddSqliteWithIdentity(builder, ConnectionStringName), + (false, false) => SqliteExtensions.AddSqliteWithIdentity(builder, ConnectionStringName, lifetime), + }; + + await Assert.That(ReferenceEquals(result, builder)).IsTrue(); + await VerifyRegistrationAsync(builder.Services, roles, lifetime); + } + + /// Verifies service-collection overloads register custom Identity types and context lifetimes. + /// Whether to configure custom roles. + /// Whether to use the overload with the default lifetime. + /// The expected context lifetime. + /// A task representing the asynchronous test. + [Test] + [Arguments(false, true, ServiceLifetime.Scoped)] + [Arguments(true, true, ServiceLifetime.Scoped)] + [Arguments(false, false, ServiceLifetime.Scoped)] + [Arguments(true, false, ServiceLifetime.Scoped)] + [Arguments(false, false, ServiceLifetime.Singleton)] + [Arguments(true, false, ServiceLifetime.Singleton)] + [Arguments(false, false, ServiceLifetime.Transient)] + [Arguments(true, false, ServiceLifetime.Transient)] + public async Task UseEntityFrameworkCoreSqlite_CustomTypes_RegisterManagersAndLifetime(bool roles, bool defaultLifetime, ServiceLifetime lifetime) + { + var configuration = new ConfigurationBuilder().AddInMemoryCollection( + [ + new KeyValuePair("ConnectionStrings:Identity", ConnectionString), + ]).Build(); + var context = new WebHostBuilderContext { Configuration = configuration }; + var services = new ServiceCollection(); + _ = services.AddLogging(); + var result = (roles, defaultLifetime) switch + { + (true, true) => SqliteExtensions.UseEntityFrameworkCoreSqlite(services, context, ConnectionStringName), + (true, false) => SqliteExtensions.UseEntityFrameworkCoreSqlite(services, context, ConnectionStringName, lifetime), + (false, true) => SqliteExtensions.UseEntityFrameworkCoreSqlite(services, context, ConnectionStringName), + (false, false) => SqliteExtensions.UseEntityFrameworkCoreSqlite(services, context, ConnectionStringName, lifetime), + }; + + await Assert.That(ReferenceEquals(result, services)).IsTrue(); + await VerifyRegistrationAsync(services, roles, lifetime); + } + + /// Verifies actual context, store, and manager resolution for the selected Identity types. + /// The configured services. + /// Whether custom role services are expected. + /// The expected context lifetime. + /// A task representing the asynchronous test. + private static async Task VerifyRegistrationAsync(IServiceCollection services, bool roles, ServiceLifetime lifetime) + { + var contextType = roles ? typeof(RoleContext) : typeof(UserContext); + ServiceDescriptor? contextDescriptor = null; + foreach (var descriptor in services) + { + if (descriptor.ServiceType == contextType) + { + contextDescriptor = descriptor; + break; + } + } + + await Assert.That(contextDescriptor).IsNotNull(); + await Assert.That(contextDescriptor!.Lifetime).IsEqualTo(lifetime); + await using var provider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); + using var firstScope = provider.CreateScope(); + using var secondScope = provider.CreateScope(); + var first = (DbContext)firstScope.ServiceProvider.GetRequiredService(contextType); + var repeated = (DbContext)firstScope.ServiceProvider.GetRequiredService(contextType); + var second = (DbContext)secondScope.ServiceProvider.GetRequiredService(contextType); + await Assert.That(first.Database.ProviderName).IsEqualTo("Microsoft.EntityFrameworkCore.Sqlite"); + await Assert.That(first.Database.GetConnectionString()).IsEqualTo(ConnectionString); + await Assert.That(ReferenceEquals(first, repeated)).IsEqualTo(lifetime != ServiceLifetime.Transient); + await Assert.That(ReferenceEquals(first, second)).IsEqualTo(lifetime == ServiceLifetime.Singleton); + await Assert.That(firstScope.ServiceProvider.GetRequiredService>()).IsNotNull(); + await Assert.That(firstScope.ServiceProvider.GetRequiredService>()).IsNotNull(); + if (roles) + { + await Assert.That(firstScope.ServiceProvider.GetRequiredService>()).IsNotNull(); + await Assert.That(firstScope.ServiceProvider.GetRequiredService>()).IsNotNull(); + } + else + { + await Assert.That(firstScope.ServiceProvider.GetService>()).IsNull(); + } + } + + /// Provides a custom application user. + public sealed class CustomUser : IdentityUser; + + /// Provides a custom application role. + public sealed class CustomRole : IdentityRole; + + /// Provides a context storing custom users without roles. + /// The configured context options. + public sealed class UserContext(DbContextOptions options) : IdentityUserContext(options); + + /// Provides a context storing custom users and roles. + /// The configured context options. + public sealed class RoleContext(DbContextOptions options) : IdentityDbContext(options); +}