diff --git a/Directory.Packages.props b/Directory.Packages.props
index 1244c88d2a..15f9016131 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -58,63 +58,63 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -123,7 +123,7 @@
-
+
@@ -133,11 +133,11 @@
-
-
-
-
-
+
+
+
+
+
@@ -170,17 +170,17 @@
-
+
-
-
+
+
-
-
-
+
+
+
@@ -195,6 +195,6 @@
-
+
\ No newline at end of file
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 =>

+## 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:
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:

@@ -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:
diff --git a/docs/en/package-version-changes.md b/docs/en/package-version-changes.md
index c4394dd753..13d9f2d373 100644
--- a/docs/en/package-version-changes.md
+++ b/docs/en/package-version-changes.md
@@ -15,10 +15,78 @@
|---------|---------|-----|
| MudBlazor | 8.0.0 | #25235 |
+**Updated:**
+
+| 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.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 |
+| 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 |
+| 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 |
+| 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
| 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
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 ba578b0000..5eb6bd5e38 100644
--- a/framework/Volo.Abp.slnx
+++ b/framework/Volo.Abp.slnx
@@ -247,6 +247,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 03d6029b69..933e1bf005 100644
--- a/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs
+++ b/framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/ChannelPool.cs
@@ -61,29 +61,45 @@ public class ChannelPool : IChannelPool, ISingletonDependency
}
}
- poolItem.Acquire();
-
if (poolItem.Channel.IsClosed)
{
- await poolItem.DisposeAsync();
- Channels.TryRemove(channelName, out _);
-
- using (await Semaphore.LockAsync())
+ ChannelPoolItem? staleItem = null;
+ try
{
- if (Channels.TryGetValue(channelName, out var existingChannelPoolItem3))
+ using (await Semaphore.LockAsync())
{
- poolItem = existingChannelPoolItem3;
+ 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);
+ }
}
- else
+ }
+ finally
+ {
+ if (staleItem != null)
{
- poolItem = new ChannelPoolItem(await CreateChannelAsync(channelName, connectionName));
- Channels.TryAdd(channelName, poolItem);
+ staleItem.WaitIfInUse(TotalDisposeWaitDuration);
+ await staleItem.DisposeAsync();
}
}
-
- poolItem.Acquire();
}
+ poolItem.Acquire();
+
return new ChannelAccessor(
poolItem.Channel,
channelName,
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..f631ce2d51
--- /dev/null
+++ b/framework/test/Volo.Abp.RabbitMQ.Tests/Volo/Abp/RabbitMQ/ChannelPool_Tests.cs
@@ -0,0 +1,359 @@
+#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
+ .DisposeAsync()
+ .Returns(_ => new ValueTask(Task.Delay(300)));
+
+ using var barrier = new Barrier(2);
+
+ var firstCaller = Task.Run(async () =>
+ {
+ barrier.SignalAndWait(RaceTimeout);
+ using var accessor = await channelPool.AcquireAsync("q");
+ });
+
+ var secondCaller = Task.Run(async () =>
+ {
+ barrier.SignalAndWait(RaceTimeout);
+ 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}");
+
+ await completed;
+ }
+
+ ///
+ /// 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(RaceTimeout);
+ 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);
+
+ var first = await channelPool.AcquireAsync("q");
+
+ 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 completed = await Task.WhenAny(secondTask, Task.Delay(RaceTimeout));
+ completed.ShouldBe(secondTask, "second caller must be unblocked after release");
+
+ var secondAccessor = await secondTask;
+ secondAccessor.Channel.ShouldBe(fixture.Channel1, "the cached channel is reused");
+
+ secondAccessor.Dispose();
+ 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_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()
+ {
+ 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(RaceTimeout);
+ 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)
+ {
+ }
+ }
+}
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..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
@@ -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;
@@ -16,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,
@@ -40,6 +42,7 @@ public class AbpSignInManager : SignInManager
SettingProvider = settingProvider;
IdentityUserManager = userManager;
CurrentTenant = currentTenant;
+ IdentityOptionsAccessor = optionsAccessor;
}
public override async Task PasswordSignInAsync(
@@ -85,6 +88,7 @@ public class AbpSignInManager : SignInManager
using (CurrentTenant.Change(user.TenantId))
{
+ await IdentityOptionsAccessor.SetAsync();
return await SignInOrTwoFactorAsync(user, isPersistent);
}
}
@@ -98,6 +102,7 @@ public class AbpSignInManager : SignInManager
using (CurrentTenant.Change(user.TenantId))
{
+ await IdentityOptionsAccessor.SetAsync();
return await PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure);
}
}
@@ -110,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)
@@ -133,6 +143,64 @@ 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))
+ {
+ await IdentityOptionsAccessor.SetAsync();
+ 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))
+ {
+ 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
+ // 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 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.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();
+ }
+ }
+
}
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..3ce774ff81
--- /dev/null
+++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/GetTwoFactorAuthenticationUser_Tests.cs
@@ -0,0 +1,135 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Shouldly;
+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();
+ }
+
+ await WriteTwoFactorCookieAsync(tenantUserId);
+
+ 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");
+ }
+
+ [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/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..1fb730538e
--- /dev/null
+++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Isolated_TwoFactor_Tests.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Shouldly;
+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/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/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..2fa0d30743
--- /dev/null
+++ b/modules/identity/test/Volo.Abp.Identity.AspNetCore.Tests/Volo/Abp/Identity/AspNetCore/Shared_SignIn_Tests.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity;
+using Shouldly;
+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 bfc0c6d51e..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
@@ -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,41 @@ 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");
+ }
+
+ [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());
+ }
+
+ [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());
+ }
}
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..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
@@ -618,6 +618,94 @@ 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();
+ }
+ }
+
+ [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,
diff --git a/npm/packs/lodash/package.json b/npm/packs/lodash/package.json
index 28e76c2779..bd3a83fd33 100644
--- a/npm/packs/lodash/package.json
+++ b/npm/packs/lodash/package.json
@@ -11,7 +11,7 @@
},
"dependencies": {
"@abp/core": "~10.3.0",
- "lodash": "^4.17.21"
+ "lodash": "^4.18.1"
},
"gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431",
"homepage": "https://abp.io",
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 f235b6b16d..ba8600a012 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
@@ -79,7 +79,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 fdb5742570..f3ae50c657 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
@@ -80,11 +80,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 181fdecc24..766d307e57 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
@@ -9,8 +9,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 5703eebd61..5ec61c4846 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 8aef80db82..9c73ca51ef 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 eb97d6f1cf..0c21fc98d5 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
@@ -12,8 +12,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 ee35cbea76..3e7280602b 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
@@ -16,7 +16,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 7425075726..f8c088bd7c 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 c866ed784c..654506d73c 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
@@ -13,8 +13,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 1a4abe213d..3623ef9af4 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
@@ -13,8 +13,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 1a3e12ca59..fa9b577dc7 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,10 +15,10 @@
-
+
-
+
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 c4f2b87496..d6628cf89d 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 61026e11ce..edd7df7c33 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 88277d221c..cbe69b3e2c 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
@@ -10,8 +10,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 9bc1e67264..3503cd12c6 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 269b48d33c..34b63a7da3 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
@@ -15,7 +15,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 @@
-
+