ok. Let's move to the https://abp.io/support/questions/10603/Duplicating-rendered-select-drop-down-controls-in-Razor-MVC-after-upgrade-to-102#answer-3a20b244-5b79-9d23-190c-8f536dbfb7aa
Thanks.
Hi,
The RMG020 warning is actually expected behavior from Mapperly. By default Mapperly uses RequiredMappingStrategy.Both, meaning every source member is expected to be mapped to a target member (and vice versa). When you map an entity like Department (which inherits ABP audit fields like TenantId, CreationTime, CreatorId, IsDeleted, LastModificationTime, etc.) to a leaner DTO such as DepartmentExcelDto, every extra source field will trigger RMG020. That's why you see 6k+ warnings piling up in one file.
The fix is to tell Mapperly to only require the target members to be mapped. Just add RequiredMappingStrategy = RequiredMappingStrategy.Target to your [Mapper] attribute:
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
public partial class DemoApplicationMappers
{
public partial DepartmentExcelDto DepartmentToDepartmentExcelDto(Department source);
// ... your other mappings
}
This one-line change will silence all of those RMG020 warnings at once, and still keeps the useful warnings (when a target property is missing a mapping).
If you have several mapper classes and want to set it once for the whole project, you can drop this into any .cs file instead of repeating the attribute on every class:
[assembly: MapperDefaults(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
This is exactly how ABP's own modules and the latest ABP Studio solution templates are set up. Every generated mapper ships with RequiredMappingStrategy = RequiredMappingStrategy.Target and inherits from MapperBase<TSource, TDestination>, e.g.:
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
public partial class DepartmentToDepartmentExcelDtoMapper : MapperBase<Department, DepartmentExcelDto>
{
public override partial DepartmentExcelDto Map(Department source);
public override partial void Map(Department source, DepartmentExcelDto destination);
}
You can check the official migration guide for more context and examples:
https://abp.io/docs/latest/release-info/migration-guides/automapper-to-mapperly
Thanks
ok, no problem. : )
hi
We will add it to the download modules list.
Thanks.
Hi,
Since you're on 10.2+, shared accounts should already ship with AbpAccountTenantResolveContributor that forces CurrentTenant = null during the 2FA flow (because the user is not yet fully authenticated when entering the 2FA code). So in theory the 2FA page should look up the host user and work fine.
Before I dig deeper, a few questions to narrow it down — could you check and share:
How do you resolve tenants? Especially: are you using domain / subdomain tenant resolution (e.g. tenant1.mydomain.com)? If yes, please share the related configuration (where you call options.Tenants, AddDomainTenantResolver, or anything that inserts a domain/subdomain contributor). If you inserted it before CurrentUserTenantResolveContributor, that will bypass the shared-mode guard above and CurrentTenant will be set to the tenant on the 2FA page, which breaks GetTwoFactorAuthenticationUserAsync.
Confirm UserSharingStrategy — please share your Configure<AbpMultiTenancyOptions>(...) block from staging. Staging and local may not be on the same configuration:
Configure<AbpMultiTenancyOptions>(options =>
{
options.IsEnabled = true;
options.UserSharingStrategy = TenantUserSharingStrategy.Shared;
});
What is the login URL on staging? Is it the host domain, a tenant subdomain, or a dedicated account domain? And what about local?
Browser DevTools check — reproduce the issue on staging, then in DevTools → Application → Cookies, check:
.AspNetCore.Identity.TwoFactorUserId cookie written after you submit username/password?/Account/SendSecurityCode, is that cookie sent back in the request headers?Domain / SameSite / Secure differences vs. local?Debug logs from the failing request on staging — please temporarily enable Debug level logs and capture the logs from the moment you submit the login form until you get redirected back to the login page. See this guide: https://abp.io/support/questions/8622/How-to-enable-Debug-logs-for-troubleshoot-problems
CurrentTenant.Id values, any CheckCurrentTenant failures (e.g. ApplicationException: Current tenant is different than given tenant. CurrentTenant.Id: ..., given tenantId: ...), SignInManager/TwoFactor related log entries, and any warnings/exceptions around the redirect.Does this happen for every 2FA account on staging, or only some of them? And does a pure host user (not in any tenant) with 2FA work on staging?
My current suspicion is either (a) a domain/subdomain tenant resolver inserted before the shared-mode guard, or (b) a cookie issue (SameSite/Domain) on staging that loses the TwoFactorUserId cookie across the redirect. The debug logs + the answers above should tell us which one.
Thanks
Hi,
Thank you so much for the detailed investigation, this is very helpful. You're absolutely right — I've confirmed this is a bug in our Pro UserPasswordChangeRequestedEventHandler.
The root cause is that this handler does not call await IdentityOptions.SetAsync(); before validating the password. SetAsync() is what loads the tenant-specific identity settings (password policy, lockout, etc.) into the IdentityOptions instance. Without it, the handler falls back to ASP.NET Core Identity's default policy, which requires digit, uppercase, lowercase and non-alphanumeric characters — exactly what you observed. Other places like IdentityUserAppService.UpdatePasswordAsync do call SetAsync(), so only this distributed event handler is affected.
We'll fix it in the next version by adding the SetAsync() call at the beginning of HandleEventAsync.
You can replace the buggy handler in your *.Domain project by doing two things:
1. Add a replacement handler class in your *.Domain project:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.Identity;
using Volo.Abp.Users;
namespace MyCompanyName.MyProjectName.Identity;
public class MyUserPasswordChangeRequestedEventHandler :
IDistributedEventHandler<UserPasswordChangeRequestedEto>,
ITransientDependency
{
public ILogger<MyUserPasswordChangeRequestedEventHandler> Logger { get; set; }
protected IIdentityUserRepository UserRepository { get; }
protected IdentityUserManager IdentityUserManager { get; }
protected IOptions<IdentityOptions> IdentityOptions { get; }
public MyUserPasswordChangeRequestedEventHandler(
IIdentityUserRepository userRepository,
IdentityUserManager identityUserManager,
IOptions<IdentityOptions> identityOptions)
{
Logger = NullLogger<MyUserPasswordChangeRequestedEventHandler>.Instance;
UserRepository = userRepository;
IdentityUserManager = identityUserManager;
IdentityOptions = identityOptions;
}
public async Task HandleEventAsync(UserPasswordChangeRequestedEto eventData)
{
try
{
await IdentityOptions.SetAsync();
if (!eventData.Password.IsNullOrEmpty())
{
var user = await UserRepository.FindByTenantIdAndUserNameAsync(eventData.UserName, eventData.TenantId);
if (user != null)
{
var errors = await ValidatePasswordAsync(user, eventData.Password);
if (errors.Any())
{
Logger.LogError("User password change failed: {userName}, reason: {reason}", eventData.UserName,
string.Join(";", errors.Select(e => e.Code)));
}
else
{
(await IdentityUserManager.RemovePasswordAsync(user)).CheckErrors();
(await IdentityUserManager.AddPasswordAsync(user, eventData.Password)).CheckErrors();
Logger.LogInformation("User password changed: {userName}", eventData.UserName);
}
}
}
}
catch (Exception e)
{
Logger.LogError("An error occurred while handling UserPasswordChangeRequestedEto: {message}", e.Message);
Logger.LogException(e);
}
}
private async Task<List<IdentityError>> ValidatePasswordAsync(IdentityUser user, string password)
{
var errors = new List<IdentityError>();
foreach (var v in IdentityUserManager.PasswordValidators)
{
var result = await v.ValidateAsync(IdentityUserManager, user, password);
if (!result.Succeeded && result.Errors.Any())
{
errors.AddRange(result.Errors);
}
}
return errors;
}
}
2. Unsubscribe the original handler in your *.Domain module's ConfigureServices:
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpDistributedEventBusOptions>(options =>
{
options.Handlers.Remove<UserPasswordChangeRequestedEventHandler>();
});
// ... your other configurations
}
The Remove call runs after AbpEventBusModule has populated the handlers list, so only your new handler will be subscribed to UserPasswordChangeRequestedEto. After this, the Set Password action from the Tenant management page will respect your tenant's password policy and actually change the password.
Regarding your other two points:
Set Password. The UI can't tell whether the background handler succeeded. We'll review this separately.PasswordValidators, which is why a simple password is accepted at creation time. We'll revisit this to make the behavior more consistent.Thanks again for the great bug report! Your ticket has been refunded. 👍
Hi,
Let me address your questions one by one:
1. Calling uow.SaveChangesAsync() without uow.CompleteAsync()
This is a bug. SaveChangesAsync() only flushes pending changes to the database within the current transaction — it does NOT commit. Only CompleteAsync() commits the transaction. If you skip CompleteAsync(), the using block will dispose the UoW and the transaction will be rolled back. So your changes are lost, but it's not a zombie transaction.
2. [UnitOfWork] attribute behavior
Yes, ABP automatically calls CompleteAsync() for you when the method returns successfully. You should NOT manually call CompleteAsync() in a [UnitOfWork] method. You only call it when you manually start a UoW via _unitOfWorkManager.Begin(...).
3. [UnitOfWork] + try/catch/throw
[UnitOfWork]
public virtual async Task Create30DaysReminderAsync(Guid fileId)
{
try { /* db ops */ }
catch { throw; }
}
When the exception propagates out of the method, ABP won't call CompleteAsync(), so the transaction rolls back correctly. The try/catch/throw is redundant here (same as not catching), but it does not break rollback behavior.
4. [UnitOfWork] + swallow exception (no rethrow)
[UnitOfWork]
public virtual async Task<SaveResult> UpdateDocumentsAsync(...)
{
try { /* db ops */ }
catch (Exception ex) { _logger.LogException(ex, LogLevel.Error); } // swallowed
}
Because the exception is swallowed, ABP thinks the method succeeded and calls CompleteAsync(), which commits the transaction. This means partial/inconsistent data can be committed. If you really want to swallow, you should explicitly call await CurrentUnitOfWork.RollbackAsync() inside the catch block, or rethrow the exception.
5. Will it create a zombie transaction?
No. As long as the UoW is wrapped in a using block (or managed by [UnitOfWork]), it will always be disposed properly — the underlying DbTransaction is released either via commit or rollback on disposal. A true zombie/orphan transaction would only happen if you bypass ABP's UoW (e.g., calling DbContext.Database.BeginTransaction() directly without disposing) or if the process crashes mid-transaction. What you're seeing is more likely long-held locks from long-running transactions, not zombie transactions.
I wrote an article that explains all of this in detail, please take a look:
https://abp.io/community/articles/understanding-transactions-in-abp-unit-of-work-0r248xsr
Thanks
Hi,
Yes, exactly. Once all Tour permissions have .RequireFeatures(TMSMSFeature.Tour), they will be hidden from the Permission Management UI when the TourModule.TourService feature is disabled.
Your diagnostic logs already confirmed this — the permissions with CheckerCount=1 were correctly filtered (IsEnabled=False).
You can remove the diagnostic file from your project now.
Thanks
Hi,
Disabling a feature alone does not automatically hide permissions. Each permission must explicitly declare .RequireFeatures() to be filtered.
From your diagnostic logs, here's what happens:
Permission WITH .RequireFeatures() — feature disabled → hidden ✓
TourService.TourMasterTransferTypes: CheckerCount=1, RequiredFeatures=[TourModule.TourService], IsEnabled=False
Permission WITHOUT .RequireFeatures() — feature disabled → still visible ✗
TourService.TourMasterRateTypes: CheckerCount=0, RequiredFeatures=(none), IsEnabled=True
The second permission has CheckerCount=0 — it doesn't know about the TourModule.TourService feature at all, so it's always visible regardless of the feature state.
To fix this, every permission that should be tied to the feature needs .RequireFeatures():
// This permission will be hidden when feature is disabled ✓
group.AddPermission("TourService.TourMasterRateTypes", L("..."))
.RequireFeatures(TMSMSFeature.Tour);
// This permission ignores feature state entirely ✗
group.AddPermission("TourService.TourMasterRateTypes", L("..."));
There's no group-level or module-level way to apply .RequireFeatures() to all permissions at once — it must be added to each permission individually.
Thanks