From 135a2f05ef2e001f29b77d746b3deefe27ddd02e Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 21 Apr 2026 09:03:55 +0800 Subject: [PATCH 01/30] Upgrade MongoDB.Driver to 3.8.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4b5cfa90f1..6750b6eeb2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -122,7 +122,7 @@ - + From d4d78c51a92360693004173c9e446782e0ee704c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Apr 2026 01:04:44 +0000 Subject: [PATCH 02/30] docs: update package version changes [skip ci] --- docs/en/package-version-changes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/package-version-changes.md b/docs/en/package-version-changes.md index 5b4722e976..fbf27ef2e0 100644 --- a/docs/en/package-version-changes.md +++ b/docs/en/package-version-changes.md @@ -11,6 +11,7 @@ | Package | Old Version | New Version | PR | |---------|-------------|-------------|-----| +| MongoDB.Driver | 3.7.1 | 3.8.0 | #25302 | | System.Security.Cryptography.Xml | 10.0.2 | 10.0.6 | #25279 | ## 10.3.0-rc.1 From fa9e4f13cfc2ef61483a18772323e64d45e6f6c9 Mon Sep 17 00:00:00 2001 From: maliming Date: Tue, 21 Apr 2026 21:09:00 +0800 Subject: [PATCH 03/30] Support shared mode lookup by id for two-factor authentication user - Add IdentityUserManager.FindSharedUserByIdAsync to resolve a user by id across tenants in shared user sharing strategy - Override AbpSignInManager.GetTwoFactorAuthenticationUserAsync to use it so the 2FA mid-flow can still find a tenant-scoped user when CurrentTenant is host - Cover the new method with unit tests --- .../Identity/AspNetCore/AbpSignInManager.cs | 20 ++++++- .../Volo/Abp/Identity/IdentityUserManager.cs | 27 +++++++++ .../Abp/Identity/IdentityUserManager_Tests.cs | 60 +++++++++++++++++++ 3 files changed, 106 insertions(+), 1 deletion(-) diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs index 4b3147c14e..e6f56c8dec 100644 --- a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs +++ b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Security.Claims; +using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; @@ -133,6 +134,23 @@ public class AbpSignInManager : SignInManager return await IdentityUserManager.FindSharedUserByLoginAsync(loginProvider, providerKey); } + public override async Task GetTwoFactorAuthenticationUserAsync() + { + var result = await Context.AuthenticateAsync(IdentityConstants.TwoFactorUserIdScheme); + if (result?.Principal == null) + { + return null; + } + + var userId = result.Principal.FindFirstValue(ClaimTypes.Name); + if (string.IsNullOrWhiteSpace(userId)) + { + return null; + } + + return await IdentityUserManager.FindSharedUserByIdAsync(userId); + } + /// /// This is to call the protection method PreSignInCheck /// diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs index c2f72366fb..46f7a635f0 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs @@ -718,4 +718,31 @@ public class IdentityUserManager : UserManager, IDomainService } } + public virtual async Task FindSharedUserByIdAsync(string userId) + { + if (MultiTenancyOptions.Value.UserSharingStrategy == TenantUserSharingStrategy.Isolated) + { + return await base.FindByIdAsync(userId); + } + + using (CurrentTenant.Change(null)) + { + using (DataFilter.Disable()) + { + var user = await base.FindByIdAsync(userId); + if (user == null) + { + return null; + } + + using (DataFilter.Enable()) + { + using (CurrentTenant.Change(user.TenantId)) + { + return await base.FindByIdAsync(userId); + } + } + } + } + } } diff --git a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs index 48da394a05..a62a9cdff8 100644 --- a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs @@ -618,6 +618,66 @@ public class SharedTenantUserSharingStrategy_IdentityUserManager_Tests : AbpIden } } + [Fact] + public async Task FindSharedUserByIdAsync_Should_Find_Tenant_User_From_Host_Context() + { + var tenantId = Guid.NewGuid(); + IdentityUser tenantUser; + + using (var uow = _unitOfWorkManager.Begin()) + { + tenantUser = await CreateUserAsync(tenantId, "shared-id-tenant-only", "shared-id-tenant-only@abp.io"); + await uow.CompleteAsync(); + } + + // Simulates the 2FA mid-flow on a Shared deployment: CurrentTenant is null + // but the user row only exists under a tenant. FindByIdAsync alone would be + // filtered out by the IMultiTenant filter, so FindSharedUserByIdAsync must + // disable the filter and still return the tenant user. + using (_currentTenant.Change(null)) + { + var user = await _identityUserManager.FindSharedUserByIdAsync(tenantUser.Id.ToString()); + + user.ShouldNotBeNull(); + user.Id.ShouldBe(tenantUser.Id); + user.TenantId.ShouldBe(tenantId); + user.UserName.ShouldBe("shared-id-tenant-only"); + } + } + + [Fact] + public async Task FindSharedUserByIdAsync_Should_Find_Host_User_From_Tenant_Context() + { + var tenantId = Guid.NewGuid(); + IdentityUser hostUser; + + using (var uow = _unitOfWorkManager.Begin()) + { + hostUser = await CreateUserAsync(null, "shared-id-host-only", "shared-id-host-only@abp.io"); + await uow.CompleteAsync(); + } + + using (_currentTenant.Change(tenantId)) + { + var user = await _identityUserManager.FindSharedUserByIdAsync(hostUser.Id.ToString()); + + user.ShouldNotBeNull(); + user.Id.ShouldBe(hostUser.Id); + user.TenantId.ShouldBeNull(); + user.UserName.ShouldBe("shared-id-host-only"); + } + } + + [Fact] + public async Task FindSharedUserByIdAsync_Should_Return_Null_For_Unknown_Id() + { + using (_currentTenant.Change(null)) + { + var user = await _identityUserManager.FindSharedUserByIdAsync(Guid.NewGuid().ToString()); + user.ShouldBeNull(); + } + } + private async Task CreateUserAsync( Guid? tenantId, string userName, From 8c508a1f4a23f691c0a063e1fe21afd8981ad31d Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 22 Apr 2026 09:23:20 +0800 Subject: [PATCH 04/30] Upgrade OpenIddict to 7.5.0 --- Directory.Packages.props | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6750b6eeb2..6ba969c647 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -132,11 +132,11 @@ - - - - - + + + + + From 63339709ba77d1ec9a8c26f440a35f64687b45d1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 22 Apr 2026 01:24:11 +0000 Subject: [PATCH 05/30] docs: update package version changes [skip ci] --- docs/en/package-version-changes.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/en/package-version-changes.md b/docs/en/package-version-changes.md index fbf27ef2e0..37ce32ba42 100644 --- a/docs/en/package-version-changes.md +++ b/docs/en/package-version-changes.md @@ -7,6 +7,16 @@ # Package Version Changes +## 10.4.0-rc.1 + +| Package | Old Version | New Version | PR | +|---------|-------------|-------------|-----| +| OpenIddict.Abstractions | 7.3.0 | 7.5.0 | #25306 | +| OpenIddict.Core | 7.3.0 | 7.5.0 | #25306 | +| OpenIddict.Server.AspNetCore | 7.3.0 | 7.5.0 | #25306 | +| OpenIddict.Validation.AspNetCore | 7.3.0 | 7.5.0 | #25306 | +| OpenIddict.Validation.ServerIntegration | 7.3.0 | 7.5.0 | #25306 | + ## 10.3.1 | Package | Old Version | New Version | PR | From cb5c17b124d181bc34481df330b74197980205d9 Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 22 Apr 2026 10:08:22 +0800 Subject: [PATCH 06/30] Add integration test for GetTwoFactorAuthenticationUserAsync in shared mode Exercises the full cookie round-trip: writes a TwoFactorUserId cookie carrying a tenant user id, then verifies that AbpSignInManager.GetTwoFactorAuthenticationUserAsync returns the tenant user when CurrentTenant is null. --- .../GetTwoFactorAuthenticationUser_Tests.cs | 62 +++++++++++++++++++ .../SharedAbpIdentityAspNetCoreTestBase.cs | 7 +++ .../SharedAbpIdentityAspNetCoreTestModule.cs | 17 +++++ .../SharedAbpIdentityAspNetCoreTestStartup.cs | 18 ++++++ .../AspNetCore/SignInTestController.cs | 20 +++++- 5 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/GetTwoFactorAuthenticationUser_Tests.cs create mode 100644 modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SharedAbpIdentityAspNetCoreTestBase.cs create mode 100644 modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SharedAbpIdentityAspNetCoreTestModule.cs create mode 100644 modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SharedAbpIdentityAspNetCoreTestStartup.cs diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/GetTwoFactorAuthenticationUser_Tests.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/GetTwoFactorAuthenticationUser_Tests.cs new file mode 100644 index 0000000000..267464c646 --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/GetTwoFactorAuthenticationUser_Tests.cs @@ -0,0 +1,62 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.Identity.AspNetCore; + +public class GetTwoFactorAuthenticationUser_Tests : SharedAbpIdentityAspNetCoreTestBase +{ + [Fact] + public async Task Should_Resolve_Tenant_User_By_Id_When_Current_Tenant_Is_Host() + { + var userRepository = GetRequiredService(); + var currentTenant = GetRequiredService(); + var unitOfWorkManager = GetRequiredService(); + + var tenantId = Guid.NewGuid(); + Guid tenantUserId; + + using (var uow = unitOfWorkManager.Begin()) + { + using (currentTenant.Change(tenantId)) + { + var user = new IdentityUser(Guid.NewGuid(), "shared-2fa-tenant-user", "shared-2fa-tenant-user@abp.io", tenantId); + await userRepository.InsertAsync(user); + tenantUserId = user.Id; + } + await uow.CompleteAsync(); + } + + var writeResponse = await Client.GetAsync($"/api/signin-test/write-two-factor-cookie?userId={tenantUserId}"); + writeResponse.EnsureSuccessStatusCode(); + + if (writeResponse.Headers.TryGetValues("Set-Cookie", out var setCookies)) + { + foreach (var cookie in setCookies) + { + Client.DefaultRequestHeaders.Add("Cookie", cookie.Split(';').First()); + } + } + + var getResponse = await Client.GetAsync("/api/signin-test/get-two-factor-user"); + getResponse.EnsureSuccessStatusCode(); + var content = await getResponse.Content.ReadAsStringAsync(); + + content.ShouldBe(tenantUserId.ToString()); + } + + [Fact] + public async Task Should_Return_Null_When_No_Two_Factor_Cookie() + { + var getResponse = await Client.GetAsync("/api/signin-test/get-two-factor-user"); + getResponse.EnsureSuccessStatusCode(); + var content = await getResponse.Content.ReadAsStringAsync(); + + content.ShouldBe("null"); + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SharedAbpIdentityAspNetCoreTestBase.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SharedAbpIdentityAspNetCoreTestBase.cs new file mode 100644 index 0000000000..451e56d268 --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SharedAbpIdentityAspNetCoreTestBase.cs @@ -0,0 +1,7 @@ +using Volo.Abp.AspNetCore.TestBase; + +namespace Volo.Abp.Identity.AspNetCore; + +public abstract class SharedAbpIdentityAspNetCoreTestBase : AbpAspNetCoreIntegratedTestBase +{ +} diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SharedAbpIdentityAspNetCoreTestModule.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SharedAbpIdentityAspNetCoreTestModule.cs new file mode 100644 index 0000000000..8ad2d55bc3 --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SharedAbpIdentityAspNetCoreTestModule.cs @@ -0,0 +1,17 @@ +using Volo.Abp.Modularity; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.Identity.AspNetCore; + +[DependsOn(typeof(AbpIdentityAspNetCoreTestModule))] +public class SharedAbpIdentityAspNetCoreTestModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.IsEnabled = true; + options.UserSharingStrategy = TenantUserSharingStrategy.Shared; + }); + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SharedAbpIdentityAspNetCoreTestStartup.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SharedAbpIdentityAspNetCoreTestStartup.cs new file mode 100644 index 0000000000..9e23739708 --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SharedAbpIdentityAspNetCoreTestStartup.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Volo.Abp.Identity.AspNetCore; + +public class SharedAbpIdentityAspNetCoreTestStartup +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddApplication(); + } + + public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) + { + app.InitializeApplication(); + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SignInTestController.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SignInTestController.cs index bfc0c6d51e..fe77766cb3 100644 --- a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SignInTestController.cs +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SignInTestController.cs @@ -1,4 +1,6 @@ -using System.Threading.Tasks; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc; @@ -27,4 +29,20 @@ public class SignInTestController : AbpController return Content(result.ToString()); } + + [Route("write-two-factor-cookie")] + public async Task WriteTwoFactorCookie(string userId) + { + var identity = new ClaimsIdentity(IdentityConstants.TwoFactorUserIdScheme); + identity.AddClaim(new Claim(ClaimTypes.Name, userId)); + await HttpContext.SignInAsync(IdentityConstants.TwoFactorUserIdScheme, new ClaimsPrincipal(identity)); + return Content("OK"); + } + + [Route("get-two-factor-user")] + public async Task GetTwoFactorUser() + { + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + return Content(user?.Id.ToString() ?? "null"); + } } From 7fd2d44dd714da22fe722d9e9122994968d6ba04 Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 22 Apr 2026 10:12:25 +0800 Subject: [PATCH 07/30] Add contract test covering login-then-two-factor lookup chain Guards against regressing the data-access contract behind the 2FA redirect bug: login must find a tenant user by user name from a host context, and the 2FA mid-flow must then resolve the same tenant user by id from the same host context. --- .../Abp/Identity/IdentityUserManager_Tests.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs index a62a9cdff8..1982080cf1 100644 --- a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs @@ -678,6 +678,34 @@ public class SharedTenantUserSharingStrategy_IdentityUserManager_Tests : AbpIden } } + [Fact] + public async Task Login_Then_TwoFactor_MidFlow_Should_Resolve_Same_Tenant_User_In_Shared_Mode() + { + // Covers the data-access contract behind the 2FA redirect bug: + // 1. login lookup (by user name) must find a tenant user from a host context, + // 2. the 2FA mid-flow lookup (by id) must then return the same tenant user + // from the same host context. Regressing either side re-opens the bug. + var tenantId = Guid.NewGuid(); + + using (var uow = _unitOfWorkManager.Begin()) + { + await CreateUserAsync(tenantId, "shared-2fa-linked", "shared-2fa-linked@abp.io"); + await uow.CompleteAsync(); + } + + using (_currentTenant.Change(null)) + { + var loginUser = await _identityUserManager.FindSharedUserByNameAsync("shared-2fa-linked"); + loginUser.ShouldNotBeNull(); + loginUser.TenantId.ShouldBe(tenantId); + + var twoFactorUser = await _identityUserManager.FindSharedUserByIdAsync(loginUser.Id.ToString()); + twoFactorUser.ShouldNotBeNull(); + twoFactorUser.Id.ShouldBe(loginUser.Id); + twoFactorUser.TenantId.ShouldBe(tenantId); + } + } + private async Task CreateUserAsync( Guid? tenantId, string userName, From d8a4e1a21cdae1b8c93b4c524861e26efb4f5a92 Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 22 Apr 2026 10:31:15 +0800 Subject: [PATCH 08/30] Bump @abp/lodash lodash dependency to ^4.18.1 --- npm/packs/lodash/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/npm/packs/lodash/package.json b/npm/packs/lodash/package.json index f8c216efe9..c66def8420 100644 --- a/npm/packs/lodash/package.json +++ b/npm/packs/lodash/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@abp/core": "~10.2.1", - "lodash": "^4.17.21" + "lodash": "^4.18.1" }, "gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", "homepage": "https://abp.io", From db07204b7ad2c6c4ccf03d2b4b3a83508a6e871c Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 22 Apr 2026 10:51:08 +0800 Subject: [PATCH 09/30] Make UserManager.FindByIdAsync shared-aware by default - Override IdentityUserManager.FindByIdAsync to fall back to a cross-tenant lookup in shared user sharing strategy so any caller that hits FindByIdAsync from a non-matching tenant context (including base SignInManager internals for TwoFactorSignInAsync and TwoFactorRecoveryCodeSignInAsync) can still resolve a tenant user by id - Drop the now-redundant AbpSignInManager.GetTwoFactorAuthenticationUserAsync override; the base implementation works automatically through the new FindByIdAsync behavior - Cover the new FindByIdAsync behavior with unit tests --- .../Identity/AspNetCore/AbpSignInManager.cs | 20 +------- .../Volo/Abp/Identity/IdentityUserManager.cs | 17 +++++++ .../Abp/Identity/IdentityUserManager_Tests.cs | 49 +++++++++++++++++++ 3 files changed, 67 insertions(+), 19 deletions(-) diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs index e6f56c8dec..4b3147c14e 100644 --- a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs +++ b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs @@ -1,5 +1,4 @@ -using System.Security.Claims; -using System.Threading.Tasks; +using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; @@ -134,23 +133,6 @@ public class AbpSignInManager : SignInManager return await IdentityUserManager.FindSharedUserByLoginAsync(loginProvider, providerKey); } - public override async Task GetTwoFactorAuthenticationUserAsync() - { - var result = await Context.AuthenticateAsync(IdentityConstants.TwoFactorUserIdScheme); - if (result?.Principal == null) - { - return null; - } - - var userId = result.Principal.FindFirstValue(ClaimTypes.Name); - if (string.IsNullOrWhiteSpace(userId)) - { - return null; - } - - return await IdentityUserManager.FindSharedUserByIdAsync(userId); - } - /// /// This is to call the protection method PreSignInCheck /// diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs index 46f7a635f0..c0470eef80 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs @@ -745,4 +745,21 @@ public class IdentityUserManager : UserManager, IDomainService } } } + + public override async Task FindByIdAsync(string userId) + { + // In shared user sharing strategy, a user id is globally unique. Fall back to + // a cross-tenant lookup so callers that hit FindByIdAsync from a non-matching + // tenant context (notably the 2FA mid-flow invoked by ASP.NET Core Identity + // base SignInManager paths) can still resolve the user by id. + // Downstream operations on the returned entity should still be scoped to + // user.TenantId explicitly via CurrentTenant.Change. + var user = await base.FindByIdAsync(userId); + if (user != null || MultiTenancyOptions.Value.UserSharingStrategy == TenantUserSharingStrategy.Isolated) + { + return user; + } + + return await FindSharedUserByIdAsync(userId); + } } diff --git a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs index 1982080cf1..803bc258a3 100644 --- a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs @@ -678,6 +678,55 @@ public class SharedTenantUserSharingStrategy_IdentityUserManager_Tests : AbpIden } } + [Fact] + public async Task FindByIdAsync_Should_Fall_Back_To_Cross_Tenant_Lookup_In_Shared_Mode() + { + // In shared user sharing strategy, UserManager.FindByIdAsync must resolve a + // tenant user even when CurrentTenant is host. This protects any caller that + // hits FindByIdAsync without an explicit shared-aware wrapper (e.g. ASP.NET + // Core Identity SignInManager internals for TwoFactorSignInAsync and + // TwoFactorRecoveryCodeSignInAsync). + var tenantId = Guid.NewGuid(); + IdentityUser tenantUser; + + using (var uow = _unitOfWorkManager.Begin()) + { + tenantUser = await CreateUserAsync(tenantId, "shared-findbyid-fallback", "shared-findbyid-fallback@abp.io"); + await uow.CompleteAsync(); + } + + using (_currentTenant.Change(null)) + { + var user = await _identityUserManager.FindByIdAsync(tenantUser.Id.ToString()); + + user.ShouldNotBeNull(); + user.Id.ShouldBe(tenantUser.Id); + user.TenantId.ShouldBe(tenantId); + } + } + + [Fact] + public async Task FindByIdAsync_Should_Return_Current_Tenant_Result_Without_Fallback_When_Found() + { + var tenantId = Guid.NewGuid(); + IdentityUser tenantUser; + + using (var uow = _unitOfWorkManager.Begin()) + { + tenantUser = await CreateUserAsync(tenantId, "shared-findbyid-hit", "shared-findbyid-hit@abp.io"); + await uow.CompleteAsync(); + } + + using (_currentTenant.Change(tenantId)) + { + var user = await _identityUserManager.FindByIdAsync(tenantUser.Id.ToString()); + + user.ShouldNotBeNull(); + user.Id.ShouldBe(tenantUser.Id); + user.TenantId.ShouldBe(tenantId); + } + } + [Fact] public async Task Login_Then_TwoFactor_MidFlow_Should_Resolve_Same_Tenant_User_In_Shared_Mode() { From 8ea6b9f025f2fc04126a6a0aaec0d673ff69c57e Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 22 Apr 2026 11:37:03 +0800 Subject: [PATCH 10/30] Make 2FA flows tenant-aware and enforce pre-checks --- .../Identity/AspNetCore/AbpSignInManager.cs | 58 ++++++++++++- .../Volo/Abp/Identity/IdentityUserManager.cs | 17 ---- .../GetTwoFactorAuthenticationUser_Tests.cs | 83 +++++++++++++++++++ .../AspNetCore/SignInTestController.cs | 14 ++++ .../Abp/Identity/IdentityUserManager_Tests.cs | 49 ----------- 5 files changed, 154 insertions(+), 67 deletions(-) diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs index 4b3147c14e..c8d3b5c275 100644 --- a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs +++ b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Security.Claims; +using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; @@ -133,6 +134,61 @@ public class AbpSignInManager : SignInManager return await IdentityUserManager.FindSharedUserByLoginAsync(loginProvider, providerKey); } + public override async Task GetTwoFactorAuthenticationUserAsync() + { + var result = await Context.AuthenticateAsync(IdentityConstants.TwoFactorUserIdScheme); + if (result?.Principal == null) + { + return null; + } + + var userId = result.Principal.FindFirstValue(ClaimTypes.Name); + if (string.IsNullOrWhiteSpace(userId)) + { + return null; + } + + return await IdentityUserManager.FindSharedUserByIdAsync(userId); + } + + public override async Task TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberClient) + { + var user = await GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + return SignInResult.Failed; + } + + using (CurrentTenant.Change(user.TenantId)) + { + return await base.TwoFactorSignInAsync(provider, code, isPersistent, rememberClient); + } + } + + public override async Task TwoFactorRecoveryCodeSignInAsync(string recoveryCode) + { + var user = await GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + return SignInResult.Failed; + } + + using (CurrentTenant.Change(user.TenantId)) + { + // Base TwoFactorRecoveryCodeSignInAsync does not invoke PreSignInCheck, which means + // AbpSignInManager's IsActive / ShouldChangePassword checks would be bypassed. Run the + // same pre-sign-in checks here so recovery-code sign-in has the same gating as the + // regular two-factor sign-in path. + var preSignInCheckResult = await PreSignInCheck(user); + if (preSignInCheckResult != null) + { + return preSignInCheckResult; + } + + return await base.TwoFactorRecoveryCodeSignInAsync(recoveryCode); + } + } + /// /// This is to call the protection method PreSignInCheck /// diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs index c0470eef80..46f7a635f0 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs @@ -745,21 +745,4 @@ public class IdentityUserManager : UserManager, IDomainService } } } - - public override async Task FindByIdAsync(string userId) - { - // In shared user sharing strategy, a user id is globally unique. Fall back to - // a cross-tenant lookup so callers that hit FindByIdAsync from a non-matching - // tenant context (notably the 2FA mid-flow invoked by ASP.NET Core Identity - // base SignInManager paths) can still resolve the user by id. - // Downstream operations on the returned entity should still be scoped to - // user.TenantId explicitly via CurrentTenant.Change. - var user = await base.FindByIdAsync(userId); - if (user != null || MultiTenancyOptions.Value.UserSharingStrategy == TenantUserSharingStrategy.Isolated) - { - return user; - } - - return await FindSharedUserByIdAsync(userId); - } } diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/GetTwoFactorAuthenticationUser_Tests.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/GetTwoFactorAuthenticationUser_Tests.cs index 267464c646..bbedc2c259 100644 --- a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/GetTwoFactorAuthenticationUser_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/GetTwoFactorAuthenticationUser_Tests.cs @@ -59,4 +59,87 @@ public class GetTwoFactorAuthenticationUser_Tests : SharedAbpIdentityAspNetCoreT content.ShouldBe("null"); } + + [Fact] + public async Task TwoFactorSignInAsync_Should_Resolve_Tenant_User_In_Shared_Mode() + { + // If the tenant switch inside AbpSignInManager.TwoFactorSignInAsync regresses, + // the base implementation would not find the user and return SignInResult.Failed. + // An inactive tenant user lets PreSignInCheck return NotAllowed, proving the user + // was actually located and inspected under its own tenant context. + var tenantUserId = await CreateInactiveTenantUserAsync("shared-2fa-signin", "shared-2fa-signin@abp.io"); + + await WriteTwoFactorCookieAsync(tenantUserId); + + var response = await Client.GetAsync("/api/signin-test/two-factor-signin?provider=Email&code=invalid"); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadAsStringAsync(); + + result.ShouldBe("NotAllowed"); + } + + [Fact] + public async Task TwoFactorRecoveryCodeSignInAsync_Should_Resolve_Tenant_User_In_Shared_Mode() + { + // AbpSignInManager.TwoFactorRecoveryCodeSignInAsync runs PreSignInCheck after locating + // the user under its own tenant context. An inactive tenant user therefore produces + // NotAllowed only when the override actually resolves the user; a regression would + // return Failed instead. + var tenantUserId = await CreateInactiveTenantUserAsync("shared-2fa-recovery", "shared-2fa-recovery@abp.io"); + + await WriteTwoFactorCookieAsync(tenantUserId); + + var response = await Client.GetAsync("/api/signin-test/two-factor-recovery-signin?recoveryCode=invalid"); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadAsStringAsync(); + + result.ShouldBe("NotAllowed"); + } + + private async Task CreateInactiveTenantUserAsync(string userName, string email) + { + return await CreateTenantUserAsync(userName, email, isActive: false); + } + + private async Task CreateTenantUserAsync(string userName, string email, bool isActive) + { + var userRepository = GetRequiredService(); + var currentTenant = GetRequiredService(); + var unitOfWorkManager = GetRequiredService(); + + var tenantId = Guid.NewGuid(); + Guid userId; + + using (var uow = unitOfWorkManager.Begin()) + { + using (currentTenant.Change(tenantId)) + { + var user = new IdentityUser(Guid.NewGuid(), userName, email, tenantId); + if (!isActive) + { + user.SetIsActive(false); + } + await userRepository.InsertAsync(user); + userId = user.Id; + } + await uow.CompleteAsync(); + } + + return userId; + } + + private async Task WriteTwoFactorCookieAsync(Guid userId) + { + var writeResponse = await Client.GetAsync($"/api/signin-test/write-two-factor-cookie?userId={userId}"); + writeResponse.EnsureSuccessStatusCode(); + + if (writeResponse.Headers.TryGetValues("Set-Cookie", out var setCookies)) + { + Client.DefaultRequestHeaders.Remove("Cookie"); + foreach (var cookie in setCookies) + { + Client.DefaultRequestHeaders.Add("Cookie", cookie.Split(';').First()); + } + } + } } diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SignInTestController.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SignInTestController.cs index fe77766cb3..8922915e6d 100644 --- a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SignInTestController.cs +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SignInTestController.cs @@ -45,4 +45,18 @@ public class SignInTestController : AbpController var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); return Content(user?.Id.ToString() ?? "null"); } + + [Route("two-factor-signin")] + public async Task TwoFactorSignIn(string provider, string code) + { + var result = await _signInManager.TwoFactorSignInAsync(provider, code, false, false); + return Content(result.ToString()); + } + + [Route("two-factor-recovery-signin")] + public async Task TwoFactorRecoverySignIn(string recoveryCode) + { + var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode); + return Content(result.ToString()); + } } diff --git a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs index 803bc258a3..1982080cf1 100644 --- a/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.Domain.Tests/Volo/Abp/Identity/IdentityUserManager_Tests.cs @@ -678,55 +678,6 @@ public class SharedTenantUserSharingStrategy_IdentityUserManager_Tests : AbpIden } } - [Fact] - public async Task FindByIdAsync_Should_Fall_Back_To_Cross_Tenant_Lookup_In_Shared_Mode() - { - // In shared user sharing strategy, UserManager.FindByIdAsync must resolve a - // tenant user even when CurrentTenant is host. This protects any caller that - // hits FindByIdAsync without an explicit shared-aware wrapper (e.g. ASP.NET - // Core Identity SignInManager internals for TwoFactorSignInAsync and - // TwoFactorRecoveryCodeSignInAsync). - var tenantId = Guid.NewGuid(); - IdentityUser tenantUser; - - using (var uow = _unitOfWorkManager.Begin()) - { - tenantUser = await CreateUserAsync(tenantId, "shared-findbyid-fallback", "shared-findbyid-fallback@abp.io"); - await uow.CompleteAsync(); - } - - using (_currentTenant.Change(null)) - { - var user = await _identityUserManager.FindByIdAsync(tenantUser.Id.ToString()); - - user.ShouldNotBeNull(); - user.Id.ShouldBe(tenantUser.Id); - user.TenantId.ShouldBe(tenantId); - } - } - - [Fact] - public async Task FindByIdAsync_Should_Return_Current_Tenant_Result_Without_Fallback_When_Found() - { - var tenantId = Guid.NewGuid(); - IdentityUser tenantUser; - - using (var uow = _unitOfWorkManager.Begin()) - { - tenantUser = await CreateUserAsync(tenantId, "shared-findbyid-hit", "shared-findbyid-hit@abp.io"); - await uow.CompleteAsync(); - } - - using (_currentTenant.Change(tenantId)) - { - var user = await _identityUserManager.FindByIdAsync(tenantUser.Id.ToString()); - - user.ShouldNotBeNull(); - user.Id.ShouldBe(tenantUser.Id); - user.TenantId.ShouldBe(tenantId); - } - } - [Fact] public async Task Login_Then_TwoFactor_MidFlow_Should_Resolve_Same_Tenant_User_In_Shared_Mode() { From ca9c2723f0f4005ae3659eef98c907677cad4293 Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 22 Apr 2026 12:56:08 +0800 Subject: [PATCH 11/30] Apply tenant identity options before base password sign-in Switching CurrentTenant to user.TenantId in PasswordSignInAsync without refreshing IdentityOptions meant that lockout, password policy and other tenant-scoped options used host values during the base sign-in call. Call IdentityOptions.SetAsync inside the tenant switch so downstream checks use the user's tenant configuration. --- .../Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs index c8d3b5c275..eeaed97d31 100644 --- a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs +++ b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs @@ -17,6 +17,7 @@ public class AbpSignInManager : SignInManager protected ISettingProvider SettingProvider { get; } protected IdentityUserManager IdentityUserManager { get; } protected ICurrentTenant CurrentTenant { get; } + protected IOptions IdentityOptionsAccessor { get; } public AbpSignInManager( IdentityUserManager userManager, @@ -41,6 +42,7 @@ public class AbpSignInManager : SignInManager SettingProvider = settingProvider; IdentityUserManager = userManager; CurrentTenant = currentTenant; + IdentityOptionsAccessor = optionsAccessor; } public override async Task PasswordSignInAsync( @@ -86,6 +88,7 @@ public class AbpSignInManager : SignInManager using (CurrentTenant.Change(user.TenantId)) { + await IdentityOptionsAccessor.SetAsync(); return await SignInOrTwoFactorAsync(user, isPersistent); } } @@ -99,6 +102,7 @@ public class AbpSignInManager : SignInManager using (CurrentTenant.Change(user.TenantId)) { + await IdentityOptionsAccessor.SetAsync(); return await PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure); } } From 8b48eb7f9896c99060c793b8d8b62039efc138d3 Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 22 Apr 2026 16:52:50 +0800 Subject: [PATCH 12/30] Add shared-user 2FA integration tests --- .../Identity/AspNetCore/AbpSignInManager.cs | 16 ++- .../AspNetCore/Isolated_TwoFactor_Tests.cs | 52 ++++++++ .../AspNetCore/Shared_SignIn_Tests.cs | 114 ++++++++++++++++++ .../AspNetCore/SignInTestController.cs | 7 ++ 4 files changed, 185 insertions(+), 4 deletions(-) create mode 100644 modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Isolated_TwoFactor_Tests.cs create mode 100644 modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Shared_SignIn_Tests.cs diff --git a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs index eeaed97d31..34bf4e0c8d 100644 --- a/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs +++ b/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpSignInManager.cs @@ -115,12 +115,17 @@ public class AbpSignInManager : SignInManager return SignInResult.Failed; } - var error = await PreSignInCheck(user); - if (error != null) + using (CurrentTenant.Change(user.TenantId)) { - return error; + await IdentityOptionsAccessor.SetAsync(); + + var error = await PreSignInCheck(user); + if (error != null) + { + return error; + } + return await SignInOrTwoFactorAsync(user, isPersistent, loginProvider, bypassTwoFactor); } - return await SignInOrTwoFactorAsync(user, isPersistent, loginProvider, bypassTwoFactor); } public virtual async Task FindByEmailAsync(string email) @@ -165,6 +170,7 @@ public class AbpSignInManager : SignInManager using (CurrentTenant.Change(user.TenantId)) { + await IdentityOptionsAccessor.SetAsync(); return await base.TwoFactorSignInAsync(provider, code, isPersistent, rememberClient); } } @@ -179,6 +185,8 @@ public class AbpSignInManager : SignInManager using (CurrentTenant.Change(user.TenantId)) { + await IdentityOptionsAccessor.SetAsync(); + // Base TwoFactorRecoveryCodeSignInAsync does not invoke PreSignInCheck, which means // AbpSignInManager's IsActive / ShouldChangePassword checks would be bypassed. Run the // same pre-sign-in checks here so recovery-code sign-in has the same gating as the diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Isolated_TwoFactor_Tests.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Isolated_TwoFactor_Tests.cs new file mode 100644 index 0000000000..0ffcc2cf57 --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Isolated_TwoFactor_Tests.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.Identity.AspNetCore; + +public class Isolated_TwoFactor_Tests : AbpIdentityAspNetCoreTestBase +{ + [Fact] + public async Task TwoFactorRecoveryCodeSignInAsync_Should_Return_NotAllowed_For_Inactive_User() + { + // The AbpSignInManager override adds PreSignInCheck to the recovery-code path (the base + // AspNetCore Identity implementation does not). This test asserts that behavior also works + // in the default (isolated) configuration so the new invariant is protected across modes. + var userManager = GetRequiredService(); + var userRepository = GetRequiredService(); + var unitOfWorkManager = GetRequiredService(); + + Guid userId; + using (var uow = unitOfWorkManager.Begin()) + { + var user = new IdentityUser(Guid.NewGuid(), "iso-recovery-inactive", "iso-recovery-inactive@abp.io"); + (await userManager.CreateAsync(user, "Iso!9Aa")).Succeeded.ShouldBeTrue(); + user.SetIsActive(false); + await userRepository.UpdateAsync(user); + userId = user.Id; + await uow.CompleteAsync(); + } + + var writeResponse = await Client.GetAsync($"/api/signin-test/write-two-factor-cookie?userId={userId}"); + writeResponse.EnsureSuccessStatusCode(); + if (writeResponse.Headers.TryGetValues("Set-Cookie", out var setCookies)) + { + Client.DefaultRequestHeaders.Remove("Cookie"); + foreach (var cookie in setCookies) + { + Client.DefaultRequestHeaders.Add("Cookie", cookie.Split(';').First()); + } + } + + var response = await Client.GetAsync("/api/signin-test/two-factor-recovery-signin?recoveryCode=invalid"); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadAsStringAsync(); + + result.ShouldBe("NotAllowed"); + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Shared_SignIn_Tests.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Shared_SignIn_Tests.cs new file mode 100644 index 0000000000..2f951345cc --- /dev/null +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Shared_SignIn_Tests.cs @@ -0,0 +1,114 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Shouldly; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.Identity.AspNetCore; + +public class Shared_SignIn_Tests : SharedAbpIdentityAspNetCoreTestBase +{ + [Fact] + public async Task PasswordSignInAsync_Should_Sign_In_Tenant_User_From_Host_Context() + { + // In shared mode, calling PasswordSignInAsync with a username while CurrentTenant is host + // must resolve the tenant user via FindSharedUserByNameAsync, apply the user's tenant + // IdentityOptions (the fix added in AbpSignInManager), and complete the sign-in. + var userManager = GetRequiredService(); + var currentTenant = GetRequiredService(); + var unitOfWorkManager = GetRequiredService(); + + var tenantId = Guid.NewGuid(); + const string userName = "shared-password-signin"; + const string password = "Shared!9Aa"; + + using (var uow = unitOfWorkManager.Begin()) + { + using (currentTenant.Change(tenantId)) + { + var user = new IdentityUser(Guid.NewGuid(), userName, userName + "@abp.io", tenantId); + (await userManager.CreateAsync(user, password)).Succeeded.ShouldBeTrue(); + } + await uow.CompleteAsync(); + } + + var response = await Client.GetAsync($"/api/signin-test/password?userName={userName}&password={password}"); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadAsStringAsync(); + + result.ShouldBe("Succeeded"); + } + + [Fact] + public async Task PasswordSignInAsync_Should_Fail_For_Wrong_Password_In_Shared_Mode() + { + var userManager = GetRequiredService(); + var currentTenant = GetRequiredService(); + var unitOfWorkManager = GetRequiredService(); + + var tenantId = Guid.NewGuid(); + const string userName = "shared-password-wrong"; + + using (var uow = unitOfWorkManager.Begin()) + { + using (currentTenant.Change(tenantId)) + { + var user = new IdentityUser(Guid.NewGuid(), userName, userName + "@abp.io", tenantId); + (await userManager.CreateAsync(user, "Shared!9Aa")).Succeeded.ShouldBeTrue(); + } + await uow.CompleteAsync(); + } + + var response = await Client.GetAsync($"/api/signin-test/password?userName={userName}&password=wrong"); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadAsStringAsync(); + + result.ShouldBe("Failed"); + } + + [Fact] + public async Task ExternalLoginSignInAsync_Should_Sign_In_Tenant_User_From_Host_Context() + { + // Covers the AbpSignInManager.ExternalLoginSignInAsync override: finds the user via + // FindSharedUserByLoginAsync, switches CurrentTenant to user.TenantId, applies tenant + // IdentityOptions, then calls PreSignInCheck + SignInOrTwoFactorAsync. + const string loginProvider = "test-provider"; + var providerKey = "ext-" + Guid.NewGuid().ToString("N").Substring(0, 8); + + var userManager = GetRequiredService(); + var currentTenant = GetRequiredService(); + var unitOfWorkManager = GetRequiredService(); + + var tenantId = Guid.NewGuid(); + + using (var uow = unitOfWorkManager.Begin()) + { + using (currentTenant.Change(tenantId)) + { + var user = new IdentityUser(Guid.NewGuid(), "shared-external-signin", "shared-external-signin@abp.io", tenantId); + (await userManager.CreateAsync(user, "Shared!9Aa")).Succeeded.ShouldBeTrue(); + (await userManager.AddLoginAsync(user, new UserLoginInfo(loginProvider, providerKey, "Test Provider"))).Succeeded.ShouldBeTrue(); + } + await uow.CompleteAsync(); + } + + var response = await Client.GetAsync($"/api/signin-test/external-login-signin?loginProvider={loginProvider}&providerKey={providerKey}"); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadAsStringAsync(); + + result.ShouldBe("Succeeded"); + } + + [Fact] + public async Task ExternalLoginSignInAsync_Should_Fail_For_Unknown_Provider_Key_In_Shared_Mode() + { + var response = await Client.GetAsync($"/api/signin-test/external-login-signin?loginProvider=unknown&providerKey=none"); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadAsStringAsync(); + + result.ShouldBe("Failed"); + } +} diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SignInTestController.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SignInTestController.cs index 8922915e6d..b2381a6a53 100644 --- a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SignInTestController.cs +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/SignInTestController.cs @@ -59,4 +59,11 @@ public class SignInTestController : AbpController var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode); return Content(result.ToString()); } + + [Route("external-login-signin")] + public async Task ExternalLoginSignIn(string loginProvider, string providerKey) + { + var result = await _signInManager.ExternalLoginSignInAsync(loginProvider, providerKey, false, false); + return Content(result.ToString()); + } } From 5c883f373d4ec9cb889271166f26a564137897cc Mon Sep 17 00:00:00 2001 From: Ma Liming Date: Wed, 22 Apr 2026 17:03:03 +0800 Subject: [PATCH 13/30] Update modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Isolated_TwoFactor_Tests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Volo/Abp/Identity/AspNetCore/Isolated_TwoFactor_Tests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Isolated_TwoFactor_Tests.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Isolated_TwoFactor_Tests.cs index 0ffcc2cf57..1fb730538e 100644 --- a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Isolated_TwoFactor_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Isolated_TwoFactor_Tests.cs @@ -2,8 +2,6 @@ using System; using System.Linq; using System.Threading.Tasks; using Shouldly; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.MultiTenancy; using Volo.Abp.Uow; using Xunit; From 1b2906fdec3c6c6324f6e74312b3d3a9669a6c85 Mon Sep 17 00:00:00 2001 From: Ma Liming Date: Wed, 22 Apr 2026 17:03:09 +0800 Subject: [PATCH 14/30] Update modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Shared_SignIn_Tests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Volo/Abp/Identity/AspNetCore/Shared_SignIn_Tests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Shared_SignIn_Tests.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Shared_SignIn_Tests.cs index 2f951345cc..2fa0d30743 100644 --- a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Shared_SignIn_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Shared_SignIn_Tests.cs @@ -2,7 +2,6 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Shouldly; -using Volo.Abp.Domain.Repositories; using Volo.Abp.MultiTenancy; using Volo.Abp.Uow; using Xunit; From 41a0b1236f247f1ee7a84b5f6c191e03f8bf936b Mon Sep 17 00:00:00 2001 From: Ma Liming Date: Wed, 22 Apr 2026 17:03:16 +0800 Subject: [PATCH 15/30] Update modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/GetTwoFactorAuthenticationUser_Tests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Identity/AspNetCore/GetTwoFactorAuthenticationUser_Tests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/GetTwoFactorAuthenticationUser_Tests.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/GetTwoFactorAuthenticationUser_Tests.cs index bbedc2c259..6bdb71a12a 100644 --- a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/GetTwoFactorAuthenticationUser_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/GetTwoFactorAuthenticationUser_Tests.cs @@ -2,7 +2,6 @@ using System; using System.Linq; using System.Threading.Tasks; using Shouldly; -using Volo.Abp.Domain.Repositories; using Volo.Abp.MultiTenancy; using Volo.Abp.Uow; using Xunit; From a613e8899c7096f2a17c0e516eda750e0c0b76b9 Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 22 Apr 2026 17:26:39 +0800 Subject: [PATCH 16/30] Reuse two-factor cookie helper in shared test --- .../GetTwoFactorAuthenticationUser_Tests.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/GetTwoFactorAuthenticationUser_Tests.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/GetTwoFactorAuthenticationUser_Tests.cs index 6bdb71a12a..3ce774ff81 100644 --- a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/GetTwoFactorAuthenticationUser_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/GetTwoFactorAuthenticationUser_Tests.cs @@ -31,16 +31,7 @@ public class GetTwoFactorAuthenticationUser_Tests : SharedAbpIdentityAspNetCoreT await uow.CompleteAsync(); } - var writeResponse = await Client.GetAsync($"/api/signin-test/write-two-factor-cookie?userId={tenantUserId}"); - writeResponse.EnsureSuccessStatusCode(); - - if (writeResponse.Headers.TryGetValues("Set-Cookie", out var setCookies)) - { - foreach (var cookie in setCookies) - { - Client.DefaultRequestHeaders.Add("Cookie", cookie.Split(';').First()); - } - } + await WriteTwoFactorCookieAsync(tenantUserId); var getResponse = await Client.GetAsync("/api/signin-test/get-two-factor-user"); getResponse.EnsureSuccessStatusCode(); From 37a0972595ddc6cbf014d1d05fd68f4447405f68 Mon Sep 17 00:00:00 2001 From: maliming Date: Wed, 22 Apr 2026 18:15:42 +0800 Subject: [PATCH 17/30] Document host-only vs tenant-admin operations in shared mode --- .../modules/account/shared-user-accounts.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/en/modules/account/shared-user-accounts.md b/docs/en/modules/account/shared-user-accounts.md index 4df18e2fdf..6f028238c6 100644 --- a/docs/en/modules/account/shared-user-accounts.md +++ b/docs/en/modules/account/shared-user-accounts.md @@ -142,6 +142,30 @@ Configure(options => ![new-user--join-strategy-inform](../../images/new-user-join-strategy-inform.png) +## Tenant Admin vs Host Admin Operations + +When the Shared strategy is enabled, a user is a **global resource** across host and all tenants. Their identity, activation state, lockout, password policy and two-factor settings live at the host level. Therefore some user management operations are restricted to host administrators and are not available to tenant administrators. + +### Host-only operations + +The following operations can only be performed by a host administrator when Shared is enabled. Both the Identity Pro UI (MVC + Blazor) and the `IdentityUserAppService` enforce this — a direct API call from a tenant context will be rejected with a `UserFriendlyException`: + +- Delete a user +- Activate / deactivate a user (`IsActive`) +- Lock / unlock a user +- Enable or disable two-factor authentication +- Change `LockoutEnabled` or `ShouldChangePasswordOnNextLogin` + +### What tenant admins can do + +- Invite users to the tenant (see the Invitation flow above) +- Manage role and organization-unit assignments within the tenant +- View audit / security logs scoped to the tenant + +### What a user can do for themselves + +Users can leave a tenant from their own account menu (`Switch Tenant` → `Leave`). Leaving marks the tenant membership as `Leaved = true` and preserves the user's host identity, so they can be re-invited later with the same `UserId`. + ## Migration Guide If you plan to migrate an existing multi-tenant application from an isolated strategy to Shared User Accounts, keep the following in mind: From 104b4d0a94f09f9aaff114cb81a812a01d2ba838 Mon Sep 17 00:00:00 2001 From: Liu Ke <56467036+scrapstation@users.noreply.github.com> Date: Wed, 22 Apr 2026 14:39:41 +0000 Subject: [PATCH 18/30] fix(#25310): revent hanging when acquiring closed channel after rabbitmq restart --- .../src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs b/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs index 03d6029b69..c06c0b6a95 100644 --- a/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs +++ b/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs @@ -61,8 +61,6 @@ public class ChannelPool : IChannelPool, ISingletonDependency } } - poolItem.Acquire(); - if (poolItem.Channel.IsClosed) { await poolItem.DisposeAsync(); @@ -80,10 +78,10 @@ public class ChannelPool : IChannelPool, ISingletonDependency Channels.TryAdd(channelName, poolItem); } } - - poolItem.Acquire(); } + poolItem.Acquire(); + return new ChannelAccessor( poolItem.Channel, channelName, From 212811d97b514547c0095e805e091e1a768d1a6b Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 23 Apr 2026 10:23:48 +0800 Subject: [PATCH 19/30] Add update and host-tenant uniqueness tests for AbpIdentityUserValidator --- .../AbpIdentityUserValidator_Tests.cs | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpIdentityUserValidator_Tests.cs b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpIdentityUserValidator_Tests.cs index 752bc3496a..b1410b2068 100644 --- a/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpIdentityUserValidator_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/AbpIdentityUserValidator_Tests.cs @@ -190,4 +190,90 @@ public class AbpIdentityUserValidator_SharedUser_Tests : AbpIdentityAspNetCoreTe result.Errors.First().Code.ShouldBe("InvalidEmail"); } } + + [Fact] + public async Task Should_Allow_Update_Without_UserName_Or_Email_Changes() + { + var tenantId = Guid.NewGuid(); + + using (_currentTenant.Change(tenantId)) + { + var user = new IdentityUser(Guid.NewGuid(), "unchanged-user", "unchanged@volosoft.com") { Name = "Original" }; + (await _identityUserManager.CreateAsync(user)).Succeeded.ShouldBeTrue(); + + user.Name = "Changed"; + (await _identityUserManager.UpdateAsync(user)).Succeeded.ShouldBeTrue(); + } + } + + [Fact] + public async Task Should_Allow_Update_Changing_UserName_To_A_Globally_Unique_Value() + { + var tenantId = Guid.NewGuid(); + + using (_currentTenant.Change(tenantId)) + { + var user = new IdentityUser(Guid.NewGuid(), "rename-start", "rename@volosoft.com"); + (await _identityUserManager.CreateAsync(user)).Succeeded.ShouldBeTrue(); + + var result = await _identityUserManager.SetUserNameAsync(user, "rename-end"); + result.Succeeded.ShouldBeTrue(); + } + } + + [Fact] + public async Task Should_Allow_Update_Changing_Email_To_A_Globally_Unique_Value() + { + var tenantId = Guid.NewGuid(); + + using (_currentTenant.Change(tenantId)) + { + var user = new IdentityUser(Guid.NewGuid(), "email-change", "email-before@volosoft.com"); + (await _identityUserManager.CreateAsync(user)).Succeeded.ShouldBeTrue(); + + var result = await _identityUserManager.SetEmailAsync(user, "email-after@volosoft.com"); + result.Succeeded.ShouldBeTrue(); + } + } + + // Host-user scenarios (TenantId == null): still must enforce global uniqueness on Create. + [Fact] + public async Task Should_Reject_Duplicate_UserName_Between_Host_User_And_Tenant_User() + { + var tenantId = Guid.NewGuid(); + const string sharedName = "host-vs-tenant-name"; + + // Host user first. + var hostUser = new IdentityUser(Guid.NewGuid(), sharedName, "host-side@volosoft.com"); + (await _identityUserManager.CreateAsync(hostUser)).Succeeded.ShouldBeTrue(); + + using (_currentTenant.Change(tenantId)) + { + var tenantUser = new IdentityUser(Guid.NewGuid(), sharedName, "tenant-side@volosoft.com"); + var result = await _identityUserManager.CreateAsync(tenantUser); + + result.Succeeded.ShouldBeFalse(); + result.Errors.Any(e => e.Code == "DuplicateUserName").ShouldBeTrue(); + } + } + + [Fact] + public async Task Should_Reject_Duplicate_Email_Between_Host_User_And_Tenant_User() + { + var tenantId = Guid.NewGuid(); + const string sharedEmail = "host-vs-tenant-email@volosoft.com"; + + var hostUser = new IdentityUser(Guid.NewGuid(), "host-email-user", sharedEmail); + (await _identityUserManager.CreateAsync(hostUser)).Succeeded.ShouldBeTrue(); + + using (_currentTenant.Change(tenantId)) + { + var tenantUser = new IdentityUser(Guid.NewGuid(), "tenant-email-user", sharedEmail); + var result = await _identityUserManager.CreateAsync(tenantUser); + + result.Succeeded.ShouldBeFalse(); + result.Errors.Any(e => e.Code == "DuplicateEmail").ShouldBeTrue(); + } + } + } From fd52e935e73299f9ca9e67dc5cb1b5be7c0dd185 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 23 Apr 2026 10:51:57 +0800 Subject: [PATCH 20/30] Bump Microsoft.* and System.* packages to 10.0.7 (CVE-2026-40372) --- Directory.Packages.props | 110 +++++++++--------- ...e.MyProjectName.Blazor.Server.Mongo.csproj | 2 +- ...anyName.MyProjectName.Blazor.Server.csproj | 4 +- ...ojectName.Blazor.WebAssembly.Client.csproj | 4 +- ...ame.Blazor.WebAssembly.Server.Mongo.csproj | 4 +- ...ojectName.Blazor.WebAssembly.Server.csproj | 6 +- ...ojectName.Blazor.WebAssembly.Shared.csproj | 2 +- ...ompanyName.MyProjectName.Host.Mongo.csproj | 2 +- .../MyCompanyName.MyProjectName.Host.csproj | 4 +- ...CompanyName.MyProjectName.Mvc.Mongo.csproj | 2 +- .../MyCompanyName.MyProjectName.Mvc.csproj | 4 +- ...ompanyName.MyProjectName.AuthServer.csproj | 2 +- ...anyName.MyProjectName.Blazor.Client.csproj | 4 +- ....MyProjectName.Blazor.Server.Tiered.csproj | 2 +- ...anyName.MyProjectName.Blazor.Server.csproj | 2 +- ....MyProjectName.Blazor.WebApp.Client.csproj | 4 +- ...ectName.Blazor.WebApp.Tiered.Client.csproj | 4 +- ....MyProjectName.Blazor.WebApp.Tiered.csproj | 4 +- ...anyName.MyProjectName.Blazor.WebApp.csproj | 2 +- .../MyCompanyName.MyProjectName.Blazor.csproj | 2 +- ...ompanyName.MyProjectName.DbMigrator.csproj | 2 +- ...anyName.MyProjectName.Domain.Shared.csproj | 2 +- ...e.MyProjectName.EntityFrameworkCore.csproj | 2 +- ...panyName.MyProjectName.HttpApi.Host.csproj | 4 +- ...yCompanyName.MyProjectName.Web.Host.csproj | 2 +- ...tName.HttpApi.Client.ConsoleTestApp.csproj | 4 +- .../MyCompanyName.MyProjectName.csproj | 2 +- .../MyCompanyName.MyProjectName.csproj | 2 +- ...ompanyName.MyProjectName.AuthServer.csproj | 4 +- ...me.MyProjectName.Blazor.Host.Client.csproj | 4 +- ...mpanyName.MyProjectName.Blazor.Host.csproj | 2 +- ...me.MyProjectName.Blazor.Server.Host.csproj | 2 +- ...panyName.MyProjectName.HttpApi.Host.csproj | 6 +- ...yCompanyName.MyProjectName.Web.Host.csproj | 2 +- ...mpanyName.MyProjectName.Web.Unified.csproj | 2 +- ...anyName.MyProjectName.Domain.Shared.csproj | 2 +- .../MyCompanyName.MyProjectName.Web.csproj | 2 +- ...ojectName.EntityFrameworkCore.Tests.csproj | 2 +- ...tName.HttpApi.Client.ConsoleTestApp.csproj | 2 +- .../MyCompanyName.MyProjectName.csproj | 2 +- 40 files changed, 111 insertions(+), 111 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6ba969c647..dddd6cfc99 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -57,63 +57,63 @@ - - - - - - - - - - + + + + + + + + + + - - - - + + + + - - - + + + - + - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + @@ -169,17 +169,17 @@ - + - - + + - - - + + + @@ -194,6 +194,6 @@ - + \ No newline at end of file diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyCompanyName.MyProjectName.Blazor.Server.Mongo.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyCompanyName.MyProjectName.Blazor.Server.Mongo.csproj index 8459c80cf0..a743bcc01c 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyCompanyName.MyProjectName.Blazor.Server.Mongo.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyCompanyName.MyProjectName.Blazor.Server.Mongo.csproj @@ -81,7 +81,7 @@ - + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj index 20d63dd3ab..51f7e486f6 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj @@ -82,11 +82,11 @@ - + - + runtime; build; native; contentfiles; analyzers compile; contentFiles; build; buildMultitargeting; buildTransitive; analyzers; native diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyCompanyName.MyProjectName.Blazor.WebAssembly.Client.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyCompanyName.MyProjectName.Blazor.WebAssembly.Client.csproj index 4d6e8b02e8..9a9770df69 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyCompanyName.MyProjectName.Blazor.WebAssembly.Client.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyCompanyName.MyProjectName.Blazor.WebAssembly.Client.csproj @@ -11,8 +11,8 @@ - - + + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.Mongo.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.Mongo.csproj index 4cfca64cd8..95546f4d78 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.Mongo.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.Mongo.csproj @@ -8,7 +8,7 @@ - + @@ -78,7 +78,7 @@ - + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.csproj index d2dcc81851..4549ab5879 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.csproj @@ -8,7 +8,7 @@ - + @@ -79,11 +79,11 @@ - + - + runtime; build; native; contentfiles; analyzers compile; contentFiles; build; buildMultitargeting; buildTransitive; analyzers; native diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Shared/MyCompanyName.MyProjectName.Blazor.WebAssembly.Shared.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Shared/MyCompanyName.MyProjectName.Blazor.WebAssembly.Shared.csproj index f47e419fd4..e6e6844dfa 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Shared/MyCompanyName.MyProjectName.Blazor.WebAssembly.Shared.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Shared/MyCompanyName.MyProjectName.Blazor.WebAssembly.Shared.csproj @@ -29,7 +29,7 @@ - + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/MyCompanyName.MyProjectName.Host.Mongo.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/MyCompanyName.MyProjectName.Host.Mongo.csproj index 481266b6ce..a7bcf351a9 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/MyCompanyName.MyProjectName.Host.Mongo.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/MyCompanyName.MyProjectName.Host.Mongo.csproj @@ -73,7 +73,7 @@ - + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/MyCompanyName.MyProjectName.Host.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/MyCompanyName.MyProjectName.Host.csproj index c8c8c9b534..7e894bc3ef 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/MyCompanyName.MyProjectName.Host.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/MyCompanyName.MyProjectName.Host.csproj @@ -74,11 +74,11 @@ - + - + runtime; build; native; contentfiles; analyzers compile; contentFiles; build; buildMultitargeting; buildTransitive; analyzers; native diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/MyCompanyName.MyProjectName.Mvc.Mongo.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/MyCompanyName.MyProjectName.Mvc.Mongo.csproj index 22ddbecc6c..3d496effec 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/MyCompanyName.MyProjectName.Mvc.Mongo.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/MyCompanyName.MyProjectName.Mvc.Mongo.csproj @@ -76,7 +76,7 @@ - + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/MyCompanyName.MyProjectName.Mvc.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/MyCompanyName.MyProjectName.Mvc.csproj index 6eb8bd408e..60a9aac413 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/MyCompanyName.MyProjectName.Mvc.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/MyCompanyName.MyProjectName.Mvc.csproj @@ -77,11 +77,11 @@ - + - + runtime; build; native; contentfiles; analyzers compile; contentFiles; build; buildMultitargeting; buildTransitive; analyzers; native diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj index 06c7bbfb73..9b9249dab6 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj @@ -41,7 +41,7 @@ - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyCompanyName.MyProjectName.Blazor.Client.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyCompanyName.MyProjectName.Blazor.Client.csproj index 0879feee0e..491c5b26e7 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyCompanyName.MyProjectName.Blazor.Client.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyCompanyName.MyProjectName.Blazor.Client.csproj @@ -14,8 +14,8 @@ - - + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyCompanyName.MyProjectName.Blazor.Server.Tiered.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyCompanyName.MyProjectName.Blazor.Server.Tiered.csproj index f8cc56776e..863ee55e3f 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyCompanyName.MyProjectName.Blazor.Server.Tiered.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyCompanyName.MyProjectName.Blazor.Server.Tiered.csproj @@ -18,7 +18,7 @@ - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj index be90ab1a5f..c885213819 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj @@ -14,7 +14,7 @@ - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Client.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Client.csproj index d8e91d2319..a7a7f9001b 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Client.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Client.csproj @@ -15,8 +15,8 @@ - - + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client.csproj index ceef0368bb..4c5843ef0e 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client.csproj @@ -15,8 +15,8 @@ - - + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.csproj index 60ef9ab125..315cff7ded 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.csproj @@ -15,12 +15,12 @@ - + - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyCompanyName.MyProjectName.Blazor.WebApp.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyCompanyName.MyProjectName.Blazor.WebApp.csproj index 0b476c8adb..9ebcd8ff4f 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyCompanyName.MyProjectName.Blazor.WebApp.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyCompanyName.MyProjectName.Blazor.WebApp.csproj @@ -15,7 +15,7 @@ - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyCompanyName.MyProjectName.Blazor.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyCompanyName.MyProjectName.Blazor.csproj index b42902d66c..a0c0672406 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyCompanyName.MyProjectName.Blazor.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyCompanyName.MyProjectName.Blazor.csproj @@ -13,7 +13,7 @@ - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.DbMigrator/MyCompanyName.MyProjectName.DbMigrator.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.DbMigrator/MyCompanyName.MyProjectName.DbMigrator.csproj index 8e30d5120e..0bfa28550b 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.DbMigrator/MyCompanyName.MyProjectName.DbMigrator.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.DbMigrator/MyCompanyName.MyProjectName.DbMigrator.csproj @@ -22,7 +22,7 @@ - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/MyCompanyName.MyProjectName.Domain.Shared.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/MyCompanyName.MyProjectName.Domain.Shared.csproj index 5bff54da57..879a0288ff 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/MyCompanyName.MyProjectName.Domain.Shared.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/MyCompanyName.MyProjectName.Domain.Shared.csproj @@ -26,7 +26,7 @@ - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/MyCompanyName.MyProjectName.EntityFrameworkCore.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/MyCompanyName.MyProjectName.EntityFrameworkCore.csproj index 3d77bbf91a..1dd5916158 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/MyCompanyName.MyProjectName.EntityFrameworkCore.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/MyCompanyName.MyProjectName.EntityFrameworkCore.csproj @@ -22,7 +22,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj index 558f1085e8..b1c3a233ac 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj @@ -13,8 +13,8 @@ - - + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj index fd67ed8d0b..13df353946 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj @@ -19,7 +19,7 @@ - + diff --git a/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp.csproj b/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp.csproj index 8d93386afe..ccf0be58ed 100644 --- a/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp.csproj +++ b/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp.csproj @@ -22,8 +22,8 @@ - - + + diff --git a/templates/console/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj b/templates/console/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj index 491b99fe17..9091e83b7b 100644 --- a/templates/console/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj +++ b/templates/console/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj @@ -13,7 +13,7 @@ - + diff --git a/templates/maui/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj b/templates/maui/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj index 6cbeb43b83..1793171fde 100644 --- a/templates/maui/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj +++ b/templates/maui/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj @@ -35,7 +35,7 @@ - + diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj index 6978bafb8b..3ca83faaf4 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj @@ -13,8 +13,8 @@ - - + + all runtime; build; native; contentfiles; analyzers diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyCompanyName.MyProjectName.Blazor.Host.Client.csproj b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyCompanyName.MyProjectName.Blazor.Host.Client.csproj index 851f327132..7a79b447cd 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyCompanyName.MyProjectName.Blazor.Host.Client.csproj +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyCompanyName.MyProjectName.Blazor.Host.Client.csproj @@ -12,8 +12,8 @@ - - + + diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host/MyCompanyName.MyProjectName.Blazor.Host.csproj b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host/MyCompanyName.MyProjectName.Blazor.Host.csproj index 2b54b7e2b9..1eb2728951 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host/MyCompanyName.MyProjectName.Blazor.Host.csproj +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host/MyCompanyName.MyProjectName.Blazor.Host.csproj @@ -13,7 +13,7 @@ - + diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/MyCompanyName.MyProjectName.Blazor.Server.Host.csproj b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/MyCompanyName.MyProjectName.Blazor.Server.Host.csproj index efcf43c5fa..5e0095ed5b 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/MyCompanyName.MyProjectName.Blazor.Server.Host.csproj +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/MyCompanyName.MyProjectName.Blazor.Server.Host.csproj @@ -17,7 +17,7 @@ - + diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj index 6413362b0b..41842c45fe 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj @@ -14,9 +14,9 @@ - - - + + + diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj index 5511bf8218..16b981a6cc 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj @@ -13,7 +13,7 @@ - + diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/MyCompanyName.MyProjectName.Web.Unified.csproj b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/MyCompanyName.MyProjectName.Web.Unified.csproj index 8c51a41819..33e4b0d85e 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/MyCompanyName.MyProjectName.Web.Unified.csproj +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/MyCompanyName.MyProjectName.Web.Unified.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/MyCompanyName.MyProjectName.Domain.Shared.csproj b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/MyCompanyName.MyProjectName.Domain.Shared.csproj index 245a02ab0b..a284171d92 100644 --- a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/MyCompanyName.MyProjectName.Domain.Shared.csproj +++ b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/MyCompanyName.MyProjectName.Domain.Shared.csproj @@ -15,7 +15,7 @@ - + diff --git a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj index 73c445396f..65e952c61e 100644 --- a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj +++ b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj @@ -22,7 +22,7 @@ - + diff --git a/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests.csproj b/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests.csproj index 738eb9185a..a9ec1ccb44 100644 --- a/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests.csproj +++ b/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp.csproj b/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp.csproj index d979841c6a..c657d63704 100644 --- a/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp.csproj +++ b/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp.csproj @@ -23,7 +23,7 @@ - + diff --git a/templates/wpf/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj b/templates/wpf/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj index c578865d23..8275968e93 100644 --- a/templates/wpf/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj +++ b/templates/wpf/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj @@ -14,7 +14,7 @@ - + From 2e80393b835386a475bc5f192c42d9c795fc54d0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 23 Apr 2026 02:52:33 +0000 Subject: [PATCH 21/30] docs: update package version changes [skip ci] --- docs/en/package-version-changes.md | 55 ++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/docs/en/package-version-changes.md b/docs/en/package-version-changes.md index 37ce32ba42..8cfbf759b7 100644 --- a/docs/en/package-version-changes.md +++ b/docs/en/package-version-changes.md @@ -11,11 +11,66 @@ | Package | Old Version | New Version | PR | |---------|-------------|-------------|-----| +| Microsoft.AspNetCore.Authentication.JwtBearer | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Authentication.OpenIdConnect | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Authorization | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Components | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Components.Authorization | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Components.Web | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Components.WebAssembly | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Components.WebAssembly.Authentication | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Components.WebAssembly.DevServer | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Components.WebAssembly.Server | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.DataProtection.StackExchangeRedis | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Mvc.NewtonsoftJson | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Mvc.Testing | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.TestHost | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.WebUtilities | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Bcl.AsyncInterfaces | 10.0.4 | 10.0.7 | #25313 | +| Microsoft.Data.Sqlite | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.EntityFrameworkCore | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.EntityFrameworkCore.Design | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.EntityFrameworkCore.InMemory | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.EntityFrameworkCore.Proxies | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.EntityFrameworkCore.Relational | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.EntityFrameworkCore.SqlServer | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.EntityFrameworkCore.Sqlite | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.EntityFrameworkCore.Tools | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Caching.Memory | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Caching.StackExchangeRedis | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Configuration.Binder | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Configuration.CommandLine | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Configuration.EnvironmentVariables | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Configuration.UserSecrets | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.DependencyInjection | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.DependencyInjection.Abstractions | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.FileProviders.Composite | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.FileProviders.Embedded | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.FileProviders.Physical | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.FileSystemGlobbing | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Hosting | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Hosting.Abstractions | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Http | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Identity.Core | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Localization | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Logging | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Logging.Abstractions | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Logging.Console | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Options | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Extensions.Options.ConfigurationExtensions | 10.0.2 | 10.0.7 | #25313 | | OpenIddict.Abstractions | 7.3.0 | 7.5.0 | #25306 | | OpenIddict.Core | 7.3.0 | 7.5.0 | #25306 | | OpenIddict.Server.AspNetCore | 7.3.0 | 7.5.0 | #25306 | | OpenIddict.Validation.AspNetCore | 7.3.0 | 7.5.0 | #25306 | | OpenIddict.Validation.ServerIntegration | 7.3.0 | 7.5.0 | #25306 | +| System.Collections.Immutable | 10.0.2 | 10.0.7 | #25313 | +| System.Management | 10.0.2 | 10.0.7 | #25313 | +| System.Security.Cryptography.Xml | 10.0.6 | 10.0.7 | #25313 | +| System.Security.Permissions | 10.0.2 | 10.0.7 | #25313 | +| System.Text.Encoding.CodePages | 10.0.2 | 10.0.7 | #25313 | +| System.Text.Encodings.Web | 10.0.2 | 10.0.7 | #25313 | +| System.Text.Json | 10.0.2 | 10.0.7 | #25313 | ## 10.3.1 From 542864f8ff52be60d97439767ae6e356c9a836f1 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 23 Apr 2026 11:22:47 +0800 Subject: [PATCH 22/30] Fix System.Management self-closing tag and bump MAUI to 10.0.51 --- Directory.Packages.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index dddd6cfc99..38b4c5066a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -67,8 +67,8 @@ - - + + @@ -194,6 +194,6 @@ - + \ No newline at end of file From 27ddaaa605dc47d873b5afcc79b5ca7dfb66cabf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 23 Apr 2026 03:24:21 +0000 Subject: [PATCH 23/30] docs: update package version changes [skip ci] --- docs/en/package-version-changes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/package-version-changes.md b/docs/en/package-version-changes.md index 8cfbf759b7..17e29d7e10 100644 --- a/docs/en/package-version-changes.md +++ b/docs/en/package-version-changes.md @@ -21,6 +21,7 @@ | Microsoft.AspNetCore.Components.WebAssembly.Authentication | 10.0.2 | 10.0.7 | #25313 | | Microsoft.AspNetCore.Components.WebAssembly.DevServer | 10.0.2 | 10.0.7 | #25313 | | Microsoft.AspNetCore.Components.WebAssembly.Server | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.AspNetCore.Components.WebView.Maui | 10.0.20 | 10.0.51 | #25313 | | Microsoft.AspNetCore.DataProtection.StackExchangeRedis | 10.0.2 | 10.0.7 | #25313 | | Microsoft.AspNetCore.Mvc.NewtonsoftJson | 10.0.2 | 10.0.7 | #25313 | | Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation | 10.0.2 | 10.0.7 | #25313 | @@ -59,6 +60,7 @@ | Microsoft.Extensions.Logging.Console | 10.0.2 | 10.0.7 | #25313 | | Microsoft.Extensions.Options | 10.0.2 | 10.0.7 | #25313 | | Microsoft.Extensions.Options.ConfigurationExtensions | 10.0.2 | 10.0.7 | #25313 | +| Microsoft.Maui.Controls | 10.0.20 | 10.0.51 | #25313 | | OpenIddict.Abstractions | 7.3.0 | 7.5.0 | #25306 | | OpenIddict.Core | 7.3.0 | 7.5.0 | #25306 | | OpenIddict.Server.AspNetCore | 7.3.0 | 7.5.0 | #25306 | From ba4c0bd3d68dfcfc0e8e92c015904115f3f4408f Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 23 Apr 2026 11:59:59 +0800 Subject: [PATCH 24/30] feat: add RabbitMQ tests and improve channel pool handling --- framework/Volo.Abp.abpmdl | 4 + framework/Volo.Abp.slnx | 1 + .../Volo/Abp/RabbitMQ/ChannelPool.cs | 19 +- .../Volo.Abp.RabbitMQ.Tests.abppkg | 3 + .../Volo.Abp.RabbitMQ.Tests.csproj | 16 + .../Volo/Abp/RabbitMQ/ChannelPool_Tests.cs | 331 ++++++++++++++++++ 6 files changed, 369 insertions(+), 5 deletions(-) create mode 100644 framework/test/Volo.Abp.RabbitMQ.Tests/Volo.Abp.RabbitMQ.Tests.abppkg create mode 100644 framework/test/Volo.Abp.RabbitMQ.Tests/Volo.Abp.RabbitMQ.Tests.csproj create mode 100644 framework/test/Volo.Abp.RabbitMQ.Tests/Volo/Abp/RabbitMQ/ChannelPool_Tests.cs diff --git a/framework/Volo.Abp.abpmdl b/framework/Volo.Abp.abpmdl index afc52ca801..0935fedade 100644 --- a/framework/Volo.Abp.abpmdl +++ b/framework/Volo.Abp.abpmdl @@ -478,6 +478,10 @@ "path": "test/Volo.Abp.ObjectMapping.Tests/Volo.Abp.ObjectMapping.Tests.abppkg", "folder": "test" }, + "Volo.Abp.RabbitMQ.Tests": { + "path": "test/Volo.Abp.RabbitMQ.Tests/Volo.Abp.RabbitMQ.Tests.abppkg", + "folder": "test" + }, "Volo.Abp.Ddd.Application.Contracts": { "path": "src/Volo.Abp.Ddd.Application.Contracts/Volo.Abp.Ddd.Application.Contracts.abppkg", "folder": "src" diff --git a/framework/Volo.Abp.slnx b/framework/Volo.Abp.slnx index 26d462fb4f..df59da0515 100644 --- a/framework/Volo.Abp.slnx +++ b/framework/Volo.Abp.slnx @@ -240,6 +240,7 @@ + diff --git a/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs b/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs index c06c0b6a95..424c1600ad 100644 --- a/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs +++ b/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; @@ -63,14 +64,22 @@ public class ChannelPool : IChannelPool, ISingletonDependency if (poolItem.Channel.IsClosed) { - await poolItem.DisposeAsync(); - Channels.TryRemove(channelName, out _); - using (await Semaphore.LockAsync()) { - if (Channels.TryGetValue(channelName, out var existingChannelPoolItem3)) + if (Channels.TryGetValue(channelName, out var currentChannelPoolItem) && + ReferenceEquals(currentChannelPoolItem, poolItem)) + { + // This caller is the first to notice the cached channel is closed; rebuild it. + await poolItem.DisposeAsync(); + Channels.TryRemove(new KeyValuePair(channelName, poolItem)); + + poolItem = new ChannelPoolItem(await CreateChannelAsync(channelName, connectionName)); + Channels.TryAdd(channelName, poolItem); + } + else if (currentChannelPoolItem != null) { - poolItem = existingChannelPoolItem3; + // Another caller already rebuilt the channel; use the fresh one. + poolItem = currentChannelPoolItem; } else { diff --git a/framework/test/Volo.Abp.RabbitMQ.Tests/Volo.Abp.RabbitMQ.Tests.abppkg b/framework/test/Volo.Abp.RabbitMQ.Tests/Volo.Abp.RabbitMQ.Tests.abppkg new file mode 100644 index 0000000000..64c1552e37 --- /dev/null +++ b/framework/test/Volo.Abp.RabbitMQ.Tests/Volo.Abp.RabbitMQ.Tests.abppkg @@ -0,0 +1,3 @@ +{ + "role": "lib.test" +} diff --git a/framework/test/Volo.Abp.RabbitMQ.Tests/Volo.Abp.RabbitMQ.Tests.csproj b/framework/test/Volo.Abp.RabbitMQ.Tests/Volo.Abp.RabbitMQ.Tests.csproj new file mode 100644 index 0000000000..0c4b2a2260 --- /dev/null +++ b/framework/test/Volo.Abp.RabbitMQ.Tests/Volo.Abp.RabbitMQ.Tests.csproj @@ -0,0 +1,16 @@ + + + + + + net10.0 + + + + + + + + + + diff --git a/framework/test/Volo.Abp.RabbitMQ.Tests/Volo/Abp/RabbitMQ/ChannelPool_Tests.cs b/framework/test/Volo.Abp.RabbitMQ.Tests/Volo/Abp/RabbitMQ/ChannelPool_Tests.cs new file mode 100644 index 0000000000..3fe52ae563 --- /dev/null +++ b/framework/test/Volo.Abp.RabbitMQ.Tests/Volo/Abp/RabbitMQ/ChannelPool_Tests.cs @@ -0,0 +1,331 @@ +#nullable enable + +using System; +using System.Threading; +using System.Threading.Tasks; +using NSubstitute; +using RabbitMQ.Client; +using Shouldly; +using Xunit; + +namespace Volo.Abp.RabbitMQ; + +public class ChannelPool_Tests +{ + private static readonly TimeSpan RaceTimeout = TimeSpan.FromSeconds(5); + + // ---- Regression tests for issue #25310 / PR #25311 -------------------- + + /// + /// After a RabbitMQ restart the cached channel becomes closed. Two callers + /// that both observe the stale poolItem must not deadlock while the pool + /// replaces it with a fresh channel. + /// + [Fact] + public async Task AcquireAsync_Should_Not_Hang_When_Channel_Closed_With_Concurrent_Callers() + { + var fixture = BuildConnectionPool(); + var channelPool = new TestChannelPool(fixture.Pool); + + using (await channelPool.AcquireAsync("q")) + { + } + + fixture.Channel1.IsClosed.Returns(true); + fixture.Channel1 + .When(x => x.DisposeAsync()) + .Do(_ => Thread.Sleep(300)); + + using var barrier = new Barrier(2); + + var firstCaller = Task.Run(async () => + { + barrier.SignalAndWait(); + using var accessor = await channelPool.AcquireAsync("q"); + }); + + var secondCaller = Task.Run(async () => + { + barrier.SignalAndWait(); + await Task.Delay(50); + using var accessor = await channelPool.AcquireAsync("q"); + }); + + var completed = Task.WhenAll(firstCaller, secondCaller); + var winner = await Task.WhenAny(completed, Task.Delay(RaceTimeout)); + + winner.ShouldBe( + completed, + $"AcquireAsync is hanging on a stale poolItem. firstCaller={firstCaller.Status}, secondCaller={secondCaller.Status}"); + } + + /// + /// When several callers race through the closed-channel recovery path, only one new + /// channel must be created and the dictionary must keep exactly the channel that callers + /// receive — otherwise the replacement leaks on the broker side. + /// + [Fact] + public async Task AcquireAsync_Should_Create_Only_One_Replacement_Channel_When_Many_Callers_Race() + { + var fixture = BuildConnectionPool(); + var channelPool = new TestChannelPool(fixture.Pool); + + using (await channelPool.AcquireAsync("q")) + { + } + + fixture.Channel1.IsClosed.Returns(true); + + const int callerCount = 8; + using var barrier = new Barrier(callerCount); + var tasks = new Task[callerCount]; + for (var i = 0; i < callerCount; i++) + { + tasks[i] = Task.Run(async () => + { + barrier.SignalAndWait(); + using var accessor = await channelPool.AcquireAsync("q"); + return accessor.Channel; + }); + } + + var channels = await Task.WhenAll(tasks); + + foreach (var channel in channels) + { + channel.ShouldBe(fixture.Channel2, "every caller must receive the single replacement channel"); + } + + fixture.CreateChannelCalls.ShouldBe( + 2, + $"exactly one initial + one replacement channel should be created, but got {fixture.CreateChannelCalls}"); + } + + // ---- Behavior guard-rails: make sure the refactor preserves core semantics ---- + + [Fact] + public async Task AcquireAsync_Should_Return_Channel_From_ConnectionPool() + { + var fixture = BuildConnectionPool(); + var channelPool = new TestChannelPool(fixture.Pool); + + using var accessor = await channelPool.AcquireAsync("q"); + + accessor.Channel.ShouldBe(fixture.Channel1); + accessor.Name.ShouldBe("q"); + fixture.CreateChannelCalls.ShouldBe(1); + } + + [Fact] + public async Task AcquireAsync_Should_Reuse_Cached_Channel_On_Subsequent_Calls() + { + var fixture = BuildConnectionPool(); + var channelPool = new TestChannelPool(fixture.Pool); + + using (var first = await channelPool.AcquireAsync("q")) + { + first.Channel.ShouldBe(fixture.Channel1); + } + using (var second = await channelPool.AcquireAsync("q")) + { + second.Channel.ShouldBe(fixture.Channel1); + } + using (var third = await channelPool.AcquireAsync("q")) + { + third.Channel.ShouldBe(fixture.Channel1); + } + + fixture.CreateChannelCalls.ShouldBe(1, "a healthy cached channel must be reused"); + } + + [Fact] + public async Task AcquireAsync_Should_Keep_Separate_PoolItems_For_Different_ChannelNames() + { + var fixture = BuildConnectionPool(); + var channelPool = new TestChannelPool(fixture.Pool); + + using var a = await channelPool.AcquireAsync("queue-a"); + using var b = await channelPool.AcquireAsync("queue-b"); + + a.Channel.ShouldBe(fixture.Channel1); + b.Channel.ShouldBe(fixture.Channel2); + a.Name.ShouldBe("queue-a"); + b.Name.ShouldBe("queue-b"); + fixture.CreateChannelCalls.ShouldBe(2); + } + + [Fact] + public async Task AcquireAsync_Should_Serialize_Concurrent_Callers_On_Same_Channel() + { + var fixture = BuildConnectionPool(); + var channelPool = new TestChannelPool(fixture.Pool); + + // First caller holds the channel for a while. + var first = await channelPool.AcquireAsync("q"); + var firstAcquiredAt = DateTime.UtcNow; + + // Second caller should block until the first one releases. + var secondTask = Task.Run(async () => + { + var accessor = await channelPool.AcquireAsync("q"); + return (accessor, acquiredAt: DateTime.UtcNow); + }); + + await Task.Delay(100); + secondTask.IsCompleted.ShouldBeFalse("second caller must block while the channel is held"); + + first.Dispose(); + var firstReleasedAt = DateTime.UtcNow; + + var completed = await Task.WhenAny(secondTask, Task.Delay(RaceTimeout)); + completed.ShouldBe(secondTask, "second caller must be unblocked after release"); + + var (secondAccessor, secondAcquiredAt) = await secondTask; + secondAccessor.Channel.ShouldBe(fixture.Channel1, "the cached channel is reused"); + secondAcquiredAt.ShouldBeGreaterThanOrEqualTo(firstReleasedAt); + + secondAccessor.Dispose(); + _ = firstAcquiredAt; + fixture.CreateChannelCalls.ShouldBe(1, "channel is reused, never recreated"); + } + + [Fact] + public async Task AcquireAsync_Should_Not_Rebuild_When_Cached_Channel_Is_Healthy() + { + var fixture = BuildConnectionPool(); + var channelPool = new TestChannelPool(fixture.Pool); + + using (await channelPool.AcquireAsync("q")) + { + } + // Channel stays open, so subsequent calls must not go through the rebuild branch. + using (await channelPool.AcquireAsync("q")) + { + } + + await fixture.Channel1.DidNotReceive().DisposeAsync(); + fixture.CreateChannelCalls.ShouldBe(1); + } + + [Fact] + public async Task AcquireAsync_Should_Propagate_Exception_When_CreateChannel_Fails() + { + var channel = Substitute.For(); + channel.IsClosed.Returns(false); + + var connection = Substitute.For(); + var attempts = 0; + connection + .CreateChannelAsync(Arg.Any(), Arg.Any()) + .Returns(_ => + { + var n = Interlocked.Increment(ref attempts); + if (n == 1) + { + throw new InvalidOperationException("broker down"); + } + return Task.FromResult(channel); + }); + + var connectionPool = Substitute.For(); + connectionPool.GetAsync(Arg.Any()).Returns(Task.FromResult(connection)); + + var channelPool = new TestChannelPool(connectionPool); + + await Should.ThrowAsync(() => channelPool.AcquireAsync("q")); + + // Subsequent call should be able to succeed (broker came back). + using var accessor = await channelPool.AcquireAsync("q"); + accessor.Channel.ShouldBe(channel); + attempts.ShouldBe(2); + } + + [Fact] + public async Task AcquireAsync_Should_Throw_After_Pool_Disposed() + { + var fixture = BuildConnectionPool(); + var channelPool = new TestChannelPool(fixture.Pool); + + await channelPool.DisposeAsync(); + + await Should.ThrowAsync(() => channelPool.AcquireAsync("q")); + } + + [Fact] + public async Task AcquireAsync_Should_Only_Create_One_Channel_Even_When_First_Callers_Race() + { + var fixture = BuildConnectionPool(); + var channelPool = new TestChannelPool(fixture.Pool); + + const int callerCount = 8; + using var barrier = new Barrier(callerCount); + var tasks = new Task[callerCount]; + for (var i = 0; i < callerCount; i++) + { + tasks[i] = Task.Run(async () => + { + barrier.SignalAndWait(); + using var accessor = await channelPool.AcquireAsync("q"); + return accessor.Channel; + }); + } + + var channels = await Task.WhenAll(tasks); + + foreach (var channel in channels) + { + channel.ShouldBe(fixture.Channel1, "the first-creation semaphore must serialize initial creation"); + } + fixture.CreateChannelCalls.ShouldBe(1); + } + + // ---- Fixture --------------------------------------------------------- + + private static ConnectionPoolFixture BuildConnectionPool() + { + var channel1 = Substitute.For(); + channel1.IsClosed.Returns(false); + + var channel2 = Substitute.For(); + channel2.IsClosed.Returns(false); + + var fixture = new ConnectionPoolFixture(channel1, channel2); + + var connection = Substitute.For(); + connection + .CreateChannelAsync(Arg.Any(), Arg.Any()) + .Returns(_ => + { + var n = Interlocked.Increment(ref fixture.CreateChannelCallsField); + return n == 1 ? Task.FromResult(channel1) : Task.FromResult(channel2); + }); + + var connectionPool = Substitute.For(); + connectionPool.GetAsync(Arg.Any()).Returns(Task.FromResult(connection)); + + fixture.Pool = connectionPool; + return fixture; + } + + private sealed class ConnectionPoolFixture + { + public IConnectionPool Pool { get; set; } = default!; + public IChannel Channel1 { get; } + public IChannel Channel2 { get; } + public int CreateChannelCallsField; + public int CreateChannelCalls => CreateChannelCallsField; + + public ConnectionPoolFixture(IChannel channel1, IChannel channel2) + { + Channel1 = channel1; + Channel2 = channel2; + } + } + + private sealed class TestChannelPool : ChannelPool + { + public TestChannelPool(IConnectionPool connectionPool) : base(connectionPool) + { + } + } +} From 4286e3f581568a9a07acbc26c82bedb424396668 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 23 Apr 2026 12:16:09 +0800 Subject: [PATCH 25/30] fix: use netstandard-compatible TryRemove overload in ChannelPool --- .../src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs b/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs index 424c1600ad..fc3440bd88 100644 --- a/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs +++ b/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; @@ -69,16 +68,14 @@ public class ChannelPool : IChannelPool, ISingletonDependency if (Channels.TryGetValue(channelName, out var currentChannelPoolItem) && ReferenceEquals(currentChannelPoolItem, poolItem)) { - // This caller is the first to notice the cached channel is closed; rebuild it. await poolItem.DisposeAsync(); - Channels.TryRemove(new KeyValuePair(channelName, poolItem)); + Channels.TryRemove(channelName, out _); poolItem = new ChannelPoolItem(await CreateChannelAsync(channelName, connectionName)); Channels.TryAdd(channelName, poolItem); } else if (currentChannelPoolItem != null) { - // Another caller already rebuilt the channel; use the fresh one. poolItem = currentChannelPoolItem; } else From 27edcf5691701f008ef1a0288a3e5d4b06418156 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 23 Apr 2026 12:29:47 +0800 Subject: [PATCH 26/30] address copilot review: dispose stale channel outside lock; async delay in tests --- .../Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs | 9 ++++++++- .../Volo/Abp/RabbitMQ/ChannelPool_Tests.cs | 6 ++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs b/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs index fc3440bd88..16cb580746 100644 --- a/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs +++ b/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs @@ -63,12 +63,14 @@ public class ChannelPool : IChannelPool, ISingletonDependency if (poolItem.Channel.IsClosed) { + ChannelPoolItem? staleItem = null; + using (await Semaphore.LockAsync()) { if (Channels.TryGetValue(channelName, out var currentChannelPoolItem) && ReferenceEquals(currentChannelPoolItem, poolItem)) { - await poolItem.DisposeAsync(); + staleItem = poolItem; Channels.TryRemove(channelName, out _); poolItem = new ChannelPoolItem(await CreateChannelAsync(channelName, connectionName)); @@ -84,6 +86,11 @@ public class ChannelPool : IChannelPool, ISingletonDependency Channels.TryAdd(channelName, poolItem); } } + + if (staleItem != null) + { + await staleItem.DisposeAsync(); + } } poolItem.Acquire(); diff --git a/framework/test/Volo.Abp.RabbitMQ.Tests/Volo/Abp/RabbitMQ/ChannelPool_Tests.cs b/framework/test/Volo.Abp.RabbitMQ.Tests/Volo/Abp/RabbitMQ/ChannelPool_Tests.cs index 3fe52ae563..62596cd038 100644 --- a/framework/test/Volo.Abp.RabbitMQ.Tests/Volo/Abp/RabbitMQ/ChannelPool_Tests.cs +++ b/framework/test/Volo.Abp.RabbitMQ.Tests/Volo/Abp/RabbitMQ/ChannelPool_Tests.cs @@ -33,8 +33,8 @@ public class ChannelPool_Tests fixture.Channel1.IsClosed.Returns(true); fixture.Channel1 - .When(x => x.DisposeAsync()) - .Do(_ => Thread.Sleep(300)); + .DisposeAsync() + .Returns(_ => new ValueTask(Task.Delay(300))); using var barrier = new Barrier(2); @@ -57,6 +57,8 @@ public class ChannelPool_Tests winner.ShouldBe( completed, $"AcquireAsync is hanging on a stale poolItem. firstCaller={firstCaller.Status}, secondCaller={secondCaller.Status}"); + + await completed; } /// From 61f9fb278c0fee070f08a608407e84164536e871 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 23 Apr 2026 12:39:37 +0800 Subject: [PATCH 27/30] address copilot review: dispose stale channel even when recreate fails --- .../Volo/Abp/RabbitMQ/ChannelPool.cs | 46 ++++++++++--------- .../Volo/Abp/RabbitMQ/ChannelPool_Tests.cs | 36 +++++++++++++++ 2 files changed, 61 insertions(+), 21 deletions(-) diff --git a/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs b/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs index 16cb580746..7bf4fc5d9b 100644 --- a/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs +++ b/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs @@ -64,32 +64,36 @@ public class ChannelPool : IChannelPool, ISingletonDependency if (poolItem.Channel.IsClosed) { ChannelPoolItem? staleItem = null; - - using (await Semaphore.LockAsync()) + try { - if (Channels.TryGetValue(channelName, out var currentChannelPoolItem) && - ReferenceEquals(currentChannelPoolItem, poolItem)) - { - staleItem = poolItem; - Channels.TryRemove(channelName, out _); - - poolItem = new ChannelPoolItem(await CreateChannelAsync(channelName, connectionName)); - Channels.TryAdd(channelName, poolItem); - } - else if (currentChannelPoolItem != null) + using (await Semaphore.LockAsync()) { - poolItem = currentChannelPoolItem; - } - else - { - poolItem = new ChannelPoolItem(await CreateChannelAsync(channelName, connectionName)); - Channels.TryAdd(channelName, poolItem); + if (Channels.TryGetValue(channelName, out var currentChannelPoolItem) && + ReferenceEquals(currentChannelPoolItem, poolItem)) + { + staleItem = poolItem; + Channels.TryRemove(channelName, out _); + + poolItem = new ChannelPoolItem(await CreateChannelAsync(channelName, connectionName)); + Channels.TryAdd(channelName, poolItem); + } + else if (currentChannelPoolItem != null) + { + poolItem = currentChannelPoolItem; + } + else + { + poolItem = new ChannelPoolItem(await CreateChannelAsync(channelName, connectionName)); + Channels.TryAdd(channelName, poolItem); + } } } - - if (staleItem != null) + finally { - await staleItem.DisposeAsync(); + if (staleItem != null) + { + await staleItem.DisposeAsync(); + } } } diff --git a/framework/test/Volo.Abp.RabbitMQ.Tests/Volo/Abp/RabbitMQ/ChannelPool_Tests.cs b/framework/test/Volo.Abp.RabbitMQ.Tests/Volo/Abp/RabbitMQ/ChannelPool_Tests.cs index 62596cd038..f001949ba1 100644 --- a/framework/test/Volo.Abp.RabbitMQ.Tests/Volo/Abp/RabbitMQ/ChannelPool_Tests.cs +++ b/framework/test/Volo.Abp.RabbitMQ.Tests/Volo/Abp/RabbitMQ/ChannelPool_Tests.cs @@ -242,6 +242,42 @@ public class ChannelPool_Tests attempts.ShouldBe(2); } + [Fact] + public async Task AcquireAsync_Should_Dispose_Stale_Channel_Even_When_Recreate_Fails() + { + var staleChannel = Substitute.For(); + staleChannel.IsClosed.Returns(false); + + var connection = Substitute.For(); + var attempts = 0; + connection + .CreateChannelAsync(Arg.Any(), Arg.Any()) + .Returns(_ => + { + var n = Interlocked.Increment(ref attempts); + if (n == 1) + { + return Task.FromResult(staleChannel); + } + throw new InvalidOperationException("broker still down"); + }); + + var connectionPool = Substitute.For(); + connectionPool.GetAsync(Arg.Any()).Returns(Task.FromResult(connection)); + + var channelPool = new TestChannelPool(connectionPool); + + using (await channelPool.AcquireAsync("q")) + { + } + + staleChannel.IsClosed.Returns(true); + + await Should.ThrowAsync(() => channelPool.AcquireAsync("q")); + + await staleChannel.Received(1).DisposeAsync(); + } + [Fact] public async Task AcquireAsync_Should_Throw_After_Pool_Disposed() { From 69adc13cbede1dd15b0c9e1f1717b26b52d7653f Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 23 Apr 2026 12:58:41 +0800 Subject: [PATCH 28/30] address copilot review: remove racy timestamp assertion in serialization test --- .../Volo/Abp/RabbitMQ/ChannelPool_Tests.cs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/framework/test/Volo.Abp.RabbitMQ.Tests/Volo/Abp/RabbitMQ/ChannelPool_Tests.cs b/framework/test/Volo.Abp.RabbitMQ.Tests/Volo/Abp/RabbitMQ/ChannelPool_Tests.cs index f001949ba1..9e7b737d74 100644 --- a/framework/test/Volo.Abp.RabbitMQ.Tests/Volo/Abp/RabbitMQ/ChannelPool_Tests.cs +++ b/framework/test/Volo.Abp.RabbitMQ.Tests/Volo/Abp/RabbitMQ/ChannelPool_Tests.cs @@ -162,32 +162,22 @@ public class ChannelPool_Tests var fixture = BuildConnectionPool(); var channelPool = new TestChannelPool(fixture.Pool); - // First caller holds the channel for a while. var first = await channelPool.AcquireAsync("q"); - var firstAcquiredAt = DateTime.UtcNow; - // Second caller should block until the first one releases. - var secondTask = Task.Run(async () => - { - var accessor = await channelPool.AcquireAsync("q"); - return (accessor, acquiredAt: DateTime.UtcNow); - }); + var secondTask = Task.Run(() => channelPool.AcquireAsync("q")); await Task.Delay(100); secondTask.IsCompleted.ShouldBeFalse("second caller must block while the channel is held"); first.Dispose(); - var firstReleasedAt = DateTime.UtcNow; var completed = await Task.WhenAny(secondTask, Task.Delay(RaceTimeout)); completed.ShouldBe(secondTask, "second caller must be unblocked after release"); - var (secondAccessor, secondAcquiredAt) = await secondTask; + var secondAccessor = await secondTask; secondAccessor.Channel.ShouldBe(fixture.Channel1, "the cached channel is reused"); - secondAcquiredAt.ShouldBeGreaterThanOrEqualTo(firstReleasedAt); secondAccessor.Dispose(); - _ = firstAcquiredAt; fixture.CreateChannelCalls.ShouldBe(1, "channel is reused, never recreated"); } From e4ccf01d2375b5c514c85c81918f260558cddfa3 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 23 Apr 2026 13:27:43 +0800 Subject: [PATCH 29/30] address copilot review: wait for stale channel release before disposal; add barrier timeouts in tests --- .../Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs | 1 + .../Volo/Abp/RabbitMQ/ChannelPool_Tests.cs | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs b/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs index 7bf4fc5d9b..933e1bf005 100644 --- a/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs +++ b/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs @@ -92,6 +92,7 @@ public class ChannelPool : IChannelPool, ISingletonDependency { if (staleItem != null) { + staleItem.WaitIfInUse(TotalDisposeWaitDuration); await staleItem.DisposeAsync(); } } diff --git a/framework/test/Volo.Abp.RabbitMQ.Tests/Volo/Abp/RabbitMQ/ChannelPool_Tests.cs b/framework/test/Volo.Abp.RabbitMQ.Tests/Volo/Abp/RabbitMQ/ChannelPool_Tests.cs index 9e7b737d74..f631ce2d51 100644 --- a/framework/test/Volo.Abp.RabbitMQ.Tests/Volo/Abp/RabbitMQ/ChannelPool_Tests.cs +++ b/framework/test/Volo.Abp.RabbitMQ.Tests/Volo/Abp/RabbitMQ/ChannelPool_Tests.cs @@ -40,13 +40,13 @@ public class ChannelPool_Tests var firstCaller = Task.Run(async () => { - barrier.SignalAndWait(); + barrier.SignalAndWait(RaceTimeout); using var accessor = await channelPool.AcquireAsync("q"); }); var secondCaller = Task.Run(async () => { - barrier.SignalAndWait(); + barrier.SignalAndWait(RaceTimeout); await Task.Delay(50); using var accessor = await channelPool.AcquireAsync("q"); }); @@ -85,7 +85,7 @@ public class ChannelPool_Tests { tasks[i] = Task.Run(async () => { - barrier.SignalAndWait(); + barrier.SignalAndWait(RaceTimeout); using var accessor = await channelPool.AcquireAsync("q"); return accessor.Channel; }); @@ -292,7 +292,7 @@ public class ChannelPool_Tests { tasks[i] = Task.Run(async () => { - barrier.SignalAndWait(); + barrier.SignalAndWait(RaceTimeout); using var accessor = await channelPool.AcquireAsync("q"); return accessor.Channel; }); From 78292ee4ef23ecd0713b49b5a2cdadc435e3c8a8 Mon Sep 17 00:00:00 2001 From: leironghua Date: Fri, 24 Apr 2026 09:32:09 +0800 Subject: [PATCH 30/30] Document: Fix elsa pro package name (#25315) fix: update package names for ABP Elsa modules in documentation --- docs/en/modules/elsa-pro.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/en/modules/elsa-pro.md b/docs/en/modules/elsa-pro.md index 6b93e42368..63e95bdb59 100644 --- a/docs/en/modules/elsa-pro.md +++ b/docs/en/modules/elsa-pro.md @@ -46,7 +46,7 @@ After adding the package references, open the module class of the project (e.g.: ### `AbpElsaAspNetCoreModule` and `AbpElsaIdentityModule` -These two modules generally will be added to your authentication project. Please add `Volo.Elsa.Abp.AspNetCore` and `Volo.Elsa.Abp.Identity` packages to your project and add the `AbpElsaAspNetCoreModule` and `AbpElsaIdentityModule` to the `DependsOn` attribute of your module class based on your project structure: +These two modules generally will be added to your authentication project. Please add `Volo.Abp.Elsa.AspNetCore` and `Volo.Abp.Elsa.Identity` packages to your project and add the `AbpElsaAspNetCoreModule` and `AbpElsaIdentityModule` to the `DependsOn` attribute of your module class based on your project structure: ```xml @@ -65,20 +65,20 @@ These two modules generally will be added to your authentication project. Please The Elsa Workflows has its own database provider, and also has a Tenant/Role/User system. They are under active development, so the ABP Elsa module is not yet fully integrated. Below is the current status of each module in the ABP's Elsa Module: -- `AbpElsaAspNetCoreModule(Volo.Elsa.Abp.AspNetCore)` module is used to integrate Elsa authentication. -- `AbpElsaIdentityModule(Volo.Elsa.Abp.Identity)` module is used to integrate ABP Identity authentication. -- `AbpElsaApplicationModule(Volo.Elsa.Abp.Application)` and `AbpElsaApplicationContractsModule(Volo.Elsa.Abp.Application.Contracts)` modules are used to define the Elsa permissions. +- `AbpElsaAspNetCoreModule(Volo.Abp.Elsa.AspNetCore)` module is used to integrate Elsa authentication. +- `AbpElsaIdentityModule(Volo.Abp.Elsa.Identity)` module is used to integrate ABP Identity authentication. +- `AbpElsaApplicationModule(Volo.Abp.Elsa.Application)` and `AbpElsaApplicationContractsModule(Volo.Abp.Elsa.Application.Contracts)` modules are used to define the Elsa permissions. The rest of the projects/modules are basically empty and will be implemented in the future based on the Elsa features: -- `AbpElsaDomainModule(Volo.Elsa.Abp.Domain)` -- `AbpElsaEntityFrameworkCoreModule(Volo.Elsa.Abp.EntityFrameworkCore)` -- `AbpElsaHttpApiModule(Volo.Elsa.Abp.HttpApi)` -- `AbpElsaHttpApiClientModule(Volo.Elsa.Abp.HttpApi.Client)` -- `AbpElsaBlazorModule(Volo.Elsa.Abp.Blazor)` -- `AbpElsaBlazorServerModule(Volo.Elsa.Abp.Blazor.Server)` -- `AbpElsaBlazorWebAssemblyModule(Volo.Elsa.Abp.Blazor.WebAssembly)` -- `AbpElsaWebModule(Volo.Elsa.Abp.Web)` +- `AbpElsaDomainModule(Volo.Abp.Elsa.Domain)` +- `AbpElsaEntityFrameworkCoreModule(Volo.Abp.Elsa.EntityFrameworkCore)` +- `AbpElsaHttpApiModule(Volo.Abp.Elsa.HttpApi)` +- `AbpElsaHttpApiClientModule(Volo.Abp.Elsa.HttpApi.Client)` +- `AbpElsaBlazorModule(Volo.Abp.Elsa.Blazor)` +- `AbpElsaBlazorServerModule(Volo.Abp.Elsa.Blazor.Server)` +- `AbpElsaBlazorWebAssemblyModule(Volo.Abp.Elsa.Blazor.WebAssembly)` +- `AbpElsaWebModule(Volo.Abp.Elsa.Web)` ## Configure the Elsa Server @@ -129,7 +129,7 @@ The Elsa Workflow API endpoints check permissions. Also, it has a `*` wildcard p The ABP Elsa module defines all permissions that are used in the Elsa workflow. You can use ABP Permission Management module to manage the permissions. -`AbpElsaAspNetCoreModule(Volo.Elsa.Abp.AspNetCore)` module will check and add these permissions to the current user's claims: +`AbpElsaAspNetCoreModule(Volo.Abp.Elsa.AspNetCore)` module will check and add these permissions to the current user's claims: ![Elsa Permissions](../images/elsa-permissions.png) @@ -158,7 +158,7 @@ Elsa Studio requires authentication and there are two ways to authenticate Elsa ##### Elsa Studio - Password Flow Authentication -The `AbpElsaIdentityModule(Volo.Elsa.Abp.Identity)` module is used to integrate with [ABP Identity module](./identity-pro.md) to check Elsa Studio *username* and *password* against ABP Identity. +The `AbpElsaIdentityModule(Volo.Abp.Elsa.Identity)` module is used to integrate with [ABP Identity module](./identity-pro.md) to check Elsa Studio *username* and *password* against ABP Identity. You need to replace `UseIdentity` with `UseAbpIdentity` when configuring Elsa in your Elsa server project as follows: