Goal: Resolve tenant by domain and use per-tenant OpenId Connect settings (Authority, ClientId, redirect/logout URIs) in a single instance with unique domains per tenant, Blazor Web App UI, OpenIddict embedded in HttpApi.Host.
Backend (HttpApi.Host) – enable domain-based tenant resolution and wildcard redirect/cors support:
using Volo.Abp.MultiTenancy;
using Volo.Abp.OpenIddict.WildcardDomains;
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
// Resolve tenant by domain/subdomain
Configure<AbpTenantResolveOptions>(options =>
{
// Example: {0}.yourdomain.com -> tenant name from subdomain
options.AddDomainTenantResolver(configuration["TenantDomain"]); // e.g. "https://{0}.api.yourdomain.com"
});
// OpenIddict wildcard domain support for redirect & post-logout redirect URIs
PreConfigure<AbpOpenIddictWildcardDomainOptions>(options =>
{
options.EnableWildcardDomainSupport = true;
// Add all app URL formats that will appear as redirect_uri/post_logout_redirect_uri
// Examples:
options.WildcardDomainsFormat.Add(configuration["WildCardDomains:Blazor"]); // "https://{0}.yourdomain.com"
options.WildcardDomainsFormat.Add(configuration["WildCardDomains:Api"]); // "https://{0}.api.yourdomain.com"
});
}
appsettings.json (examples):
{
"TenantDomain": "https://{0}.yourdomain.com", // or "https://{0}.api.yourdomain.com" depending on which host resolves tenants
"WildCardDomains": {
"Blazor": "https://{0}.yourdomain.com",
"Api": "https://{0}.api.yourdomain.com"
},
"AuthServer": {
"Authority": "https://{0}.api.yourdomain.com", // embedded OpenIddict issuer format
"RequireHttpsMetadata": "true"
}
}
context.Services.Configure<OpenIdConnectOptions>("oidc", options =>
{
var prevLogin = options.Events.OnRedirectToIdentityProvider;
options.Events.OnRedirectToIdentityProvider = async ctx =>
{
var cfg = ctx.HttpContext.RequestServices.GetRequiredService<IConfiguration>();
var currentTenant = ctx.HttpContext.RequestServices.GetRequiredService<ICurrentTenant>();
var baseAuthority = cfg["AuthServer:Authority"]!.EnsureEndsWith('/') + "connect/authorize";
ctx.ProtocolMessage.IssuerAddress = baseAuthority;
var tenantDomainFormat = cfg["TenantDomain"];
if (currentTenant.IsAvailable && !string.IsNullOrEmpty(tenantDomainFormat))
{
ctx.ProtocolMessage.IssuerAddress = ctx.ProtocolMessage.IssuerAddress.Replace("{0}", currentTenant.Name);
}
else
{
ctx.ProtocolMessage.IssuerAddress = ctx.ProtocolMessage.IssuerAddress.Replace("{0}.", string.Empty);
}
if (prevLogin != null) await prevLogin(ctx);
};
var prevLogout = options.Events.OnRedirectToIdentityProviderForSignOut;
options.Events.OnRedirectToIdentityProviderForSignOut = async ctx =>
{
var cfg = ctx.HttpContext.RequestServices.GetRequiredService<IConfiguration>();
var currentTenant = ctx.HttpContext.RequestServices.GetRequiredService<ICurrentTenant>();
var baseLogout = cfg["AuthServer:Authority"]!.EnsureEndsWith('/') + "connect/logout";
ctx.ProtocolMessage.IssuerAddress = baseLogout;
var tenantDomainFormat = cfg["TenantDomain"];
if (currentTenant.IsAvailable && !string.IsNullOrEmpty(tenantDomainFormat))
{
ctx.ProtocolMessage.IssuerAddress = ctx.ProtocolMessage.IssuerAddress.Replace("{0}", currentTenant.Name);
}
else
{
ctx.ProtocolMessage.IssuerAddress = ctx.ProtocolMessage.IssuerAddress.Replace("{0}.", string.Empty);
}
if (prevLogout != null) await prevLogout(ctx);
};
});
Note: For Blazor WebAssembly, configure the client at startup (below).
private static string[] Protocols = { "http://", "https://" };
private static string GetTenantSegment(string baseUrl)
{
var host = baseUrl;
foreach (var p in Protocols) if (host.StartsWith(p, StringComparison.OrdinalIgnoreCase)) host = host.Substring(p.Length);
var parts = host.Split('.');
// If pattern is tenant.domain.tld -> odd length implies first part is tenant
return parts.Length % 2 == 1 ? parts[0] : null;
}
private static string ReplaceTenant(string patternUrl, string currentBase)
{
var tenant = GetTenantSegment(currentBase);
return tenant != null ? patternUrl.Replace("{0}", tenant) : patternUrl.Replace("{0}.", string.Empty);
}
private static void ConfigureAuthentication(WebAssemblyHostBuilder builder)
{
builder.Services.AddOidcAuthentication(options =>
{
builder.Configuration.Bind("AuthServer", options.ProviderOptions);
// Override Authority with tenant-specific domain
var currentBase = builder.HostEnvironment.BaseAddress;
var configuredAuthority = builder.Configuration["AuthServer:Authority"]; // e.g. "https://{0}.api.yourdomain.com"
options.ProviderOptions.Authority = ReplaceTenant(configuredAuthority, currentBase);
});
}
private static void ConfigureRemoteServices(WebAssemblyHostBuilder builder)
{
Configure<AbpRemoteServiceOptions>(opts =>
{
var baseConfigured = builder.Configuration["RemoteServices:Default:BaseUrl"]; // e.g. "https://{0}.api.yourdomain.com"
opts.RemoteServices.Default = new RemoteServiceConfiguration(ReplaceTenant(baseConfigured, builder.HostEnvironment.BaseAddress));
});
}
appsettings.json (Blazor WASM):
{
"App": { "SelfUrl": "https://{0}.yourdomain.com" },
"AuthServer": {
"Authority": "https://{0}.api.yourdomain.com",
"ClientId": "YourClient_Wasm",
"ResponseType": "code"
},
"RemoteServices": {
"Default": { "BaseUrl": "https://{0}.api.yourdomain.com" }
}
}
OpenIddict client seeding – allow wildcard redirect/logout URIs: With AbpOpenIddictWildcardDomainOptions enabled on the AuthServer/Host, you don’t need to seed every tenant domain. Seed clients with RootUrl formats or just enable wildcard formats via options as shown above. This prevents “invalid redirect_uri” for per-tenant domains.
If you validate access tokens on separate hosts with wildcard issuer: When tokens are validated by an API that uses a format Authority (https://{0}.api.yourdomain.com), configure issuer validation to accept the wildcard issuer. If you’re not using the same web host for validation or need strict issuer validation with a single scheme, use Owl.TokenWildcardIssuerValidator:
// using Owl.TokenWildcardIssuerValidator;
context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = configuration["AuthServer:Authority"]; // "https://{0}.api.yourdomain.com"
options.RequireHttpsMetadata = true;
options.TokenValidationParameters.IssuerValidator = TokenWildcardIssuerValidator.IssuerValidator;
options.TokenValidationParameters.ValidIssuers = new[]
{
"https://{0}.api.yourdomain.com/"
};
});
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
public class Card : FullAuditedAggregateRoot<Guid>
{
public Guid? BankId { get; set; }
public virtual Bank? Bank { get; set; }
public virtual ICollection<CardVariant> Variants { get; set; }
public Card()
{
Variants = new List<CardVariant>(); // important
}
}
public class CardWithNavigationProperties
{
public Card Card { get; set; } = default!;
public Bank? Bank { get; set; }
public List<CardVariant> Variants { get; set; } = new(); // important
}
- Do the same for generated DTOs if they have child collection properties:
public class CardWithNavigationPropertiesDto
{
public CardDto Card { get; set; } = default!;
public BankDto? Bank { get; set; }
public List<CardVariantDto> Variants { get; set; } = new(); // important
}
var query = from card in _cardRepository
join bank in _bankRepository on card.BankId equals bank.Id into bankJoin
from bank in bankJoin.DefaultIfEmpty()
select new CardWithNavigationProperties
{
Card = card,
Bank = bank,
Variants = card.Variants ?? new List<CardVariant>() // important
};
[Mapper]
public partial class CardWithNavigationPropertiesMapper
: IAbpMapperlyMapper<CardWithNavigationProperties, CardWithNavigationPropertiesDto>, ITransientDependency
{
[MapperIgnoreSource(nameof(CardWithNavigationProperties.Variants))]
public partial CardWithNavigationPropertiesDto Map(CardWithNavigationProperties source);
public partial void Map(CardWithNavigationProperties source, CardWithNavigationPropertiesDto destination);
public void AfterMap(CardWithNavigationProperties source, CardWithNavigationPropertiesDto destination)
{
// Safe map of collection
var items = source.Variants ?? new List<CardVariant>();
destination.Variants = items.Count == 0
? new List<CardVariantDto>()
: items.Select(MapCardVariant).ToList();
}
private partial CardVariantDto MapCardVariant(CardVariant source);
}
This ensures the generated foreach never receives a null collection.
Why it happens now
Quick checklist
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Identity;
using Volo.Abp.Account.Web.Pages.Account;
using Volo.Abp;
using Volo.Abp.ExceptionHandling;
namespace MyCompany.MyProject.Web.Pages.Account
{
// Works for Login/Register/Manage pages that inherit AccountPageModel
public class MyAccountPageModel : AccountPageModel
{
protected override string GetLocalizedExceptionMessage(Exception exception)
{
// handle ABP-wrapped Identity errors
if (exception is AbpIdentityResultException abpIdentityEx)
{
// Collect each error description and format as separate lines
var lines = abpIdentityEx
.IdentityResult
.Errors
.Select(e => L[e.Description].Value) // localize each description
.ToArray();
if (lines.Length > 0)
{
return string.Join(Environment.NewLine, lines);
}
}
// fallback to the default implementation
return base.GetLocalizedExceptionMessage(exception);
}
}
}
Then place Razor pages (e.g., Register.cshtml, Login.cshtml, ChangePassword.cshtml) under Pages/Account in your HttpApi.Host and set their @model to your derived PageModel (e.g., CustomRegisterModel : RegisterModel) which in turn can use the above override via a shared base.
ABP already converts IdentityResult to AbpIdentityResultException; you only need to detect it and build your own message list. AbpIdentityResultExtensions exposes helpers to format errors if you prefer to reuse.
Add a custom password validator and return your own error messages
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Identity;
public class MyPasswordValidator : IPasswordValidator<IdentityUser>, ITransientDependency
{
public Task<IdentityResult> ValidateAsync(UserManager<IdentityUser> manager, IdentityUser user, string password)
{
var errors = new List<IdentityError>();
// Example: built-in checks are already configured by IdentityOptions.
// Add your own custom rule:
if (password?.Contains("my-disallowed-fragment", StringComparison.OrdinalIgnoreCase) == true)
{
errors.Add(new IdentityError
{
Code = "MyCustomPasswordRule",
Description = "My custom password validation error message."
});
}
return Task.FromResult(errors.Count == 0 ? IdentityResult.Success : IdentityResult.Failed(errors.ToArray()));
}
}
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Identity;
using Volo.Abp.Modularity;
[DependsOn(typeof(AbpIdentityApplicationModule))]
public class MyProjectIdentityModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
PreConfigure<IdentityBuilder>(builder =>
{
builder.AddPasswordValidator<MyPasswordValidator>();
// you can also add user validators: .AddUserValidator<YourUserValidator>()
});
}
}
Localization: you can localize your custom error’s Description with the ABP localization system (e.g., L["MyCustomPasswordRuleMessage"]) before returning it, or return a code and map it to a localized string in your overridden GetLocalizedExceptionMessage.
Notes on sources/extension points
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
Checklist to identify and fix:
If you need to ensure the local form always appears for troubleshooting, you can temporarily override the login page as documented and explicitly render the form when you know it should be visible.
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
Example for Account Pro (Public + Admin Web) on v9.3.6:
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AspNetCore.Mvc; // for AddApplicationPartIfNotExists
using Volo.Abp.Modularity;
using Volo.Abp.Account.Pro.Public.Web;
using Volo.Abp.Account.Pro.Admin.Web;
[DependsOn(
typeof(AccountPublicWebModule),
typeof(AccountAdminWebModule)
)]
public class MyProjectWebModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
// Ensure MVC can discover Razor Pages/Views/Controllers from the module source projects
context.Services
.AddMvc()
.AddApplicationPartIfNotExists(typeof(AccountPublicWebModule).Assembly)
.AddApplicationPartIfNotExists(typeof(AccountAdminWebModule).Assembly);
// If you also have HttpApi Controllers from module source, keep conventional controllers:
Configure<AbpAspNetCoreMvcOptions>(options =>
{
options.ConventionalControllers.Create(typeof(MyProjectWebModule).Assembly);
});
}
}
Notes:
If your host only uses the Public Web part, you can omit the Admin one and keep only AccountPublicWebModule’s assembly in AddApplicationPartIfNotExists.
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
Apply and verify these steps in your Web project that hosts the UI (the one that serves /Account/Login):
A) Ensure the required middleware order in Configure pipeline In your Web host (e.g., MyProject.Web), Configure method must include, in this order:
A missing UseAuthentication/UseAuthorization or UseConfiguredEndpoints will result in POST not reaching the Razor Page handler.
B) Add MVC Application Parts for module source projects When you included Account.Pro source, ensure MVC discovers its pages/controllers:
public override void ConfigureServices(ServiceConfigurationContext context) { Configure<AbpAspNetCoreMvcOptions>(options => { options.ConventionalControllers.Create(typeof(MyProjectWebModule).Assembly); });
// Ensure MVC can locate Razor Pages/Views from module projects
context.Services.AddMvc()
.AddApplicationPartIfNotExists(typeof(Volo.Abp.Account.Pro.Public.Web.AccountPublicWebModule).Assembly)
.AddApplicationPartIfNotExists(typeof(Volo.Abp.Account.Pro.Admin.Web.AccountAdminWebModule).Assembly)
.AddControllersAsServices();
}
If your module class names differ (due to namespace), add the corresponding Public.Web/Admin.Web assemblies from the Account.Pro source you added. Without these ApplicationParts, routing/view discovery can fail and the POST handler might not be mapped.
C) Verify authentication schemes after source inclusion Account.Pro’s login relies on Identity cookie scheme for local login and IdentityConstants.ExternalScheme for external flows. Ensure you didn’t inadvertently override DefaultScheme/DefaultChallengeScheme in your host. A safe configuration:
public override void ConfigureServices(ServiceConfigurationContext context) { Configure<AuthenticationOptions>(options => { // Do not set DefaultChallengeScheme to OIDC for the whole app unless you know why. // Let AccountPro render external providers and challenge only when selected. });
context.Services.AddAuthentication(); // keep defaults from ABP/Identity
}
If you integrate an external provider (OIDC, Azure AD), register it but don’t globally set DefaultChallengeScheme. For OIDC, make sure SignInScheme is IdentityConstants.ExternalScheme:
context.Services.AddAuthentication() .AddOpenIdConnect("oidc", options => { // ... your authority, client id/secret, scopes options.SignInScheme = Microsoft.AspNetCore.Identity.IdentityConstants.ExternalScheme; });
D) Confirm the Razor Page form posts antiforgery
E) Check endpoint mapping for Razor Pages If you customized endpoint registration, ensure MapAbpEndpoints (or UseConfiguredEndpoints) is called. If you use endpoint routing explicitly, make sure you have endpoints.MapRazorPages(); or the ABP equivalent.
F) Clear caches and recompile
G) Quick diagnostics
If you also enabled “External Logins” using Account Pro settings and you recently upgraded, and you still get silent redirects, check your AbpUserLogins table for duplicated or stale IdentityUserLogin entries. Clearing conflicting entries fixed a similar issue after upgrades in another case.
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
You do not need a custom IAuditingStore just because you want to persist audit logs into a different SQL Server database. ABP’s EF Core integration already supports multiple databases and separate DbContexts per module.
Recommended approach (ABP 9.1.1):
[DependsOn(
typeof(AbpEntityFrameworkCoreSqlServerModule),
typeof(AbpEntityFrameworkCoreModule),
typeof(AbpAuditLoggingEntityFrameworkCoreModule) // important
)]
public class YourAppEntityFrameworkCoreModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpDbContextOptions>(options =>
{
// Default DB for your main DbContext(s)
options.Configure<YourAppDbContext>(c => c.UseSqlServer());
// Configure the audit logging DbContext to use a different connection string
options.Configure<AbpAuditLoggingDbContext>(c =>
{
c.UseSqlServer();
});
// Fallback provider if needed
options.UseSqlServer();
});
// Map connection strings by name (Default vs. AuditLogging)
// appsettings.json:
// "ConnectionStrings": {
// "Default": "Server=...;Database=MainDb;...",
// "AuditLogging": "Server=...;Database=AuditDb;..."
// }
Configure<AbpDbConnectionOptions>(opt =>
{
// Tell ABP to use "AuditLogging" connection for AbpAuditLoggingDbContext
opt.Databases.Configure("AuditLogging", database =>
{
database.MappedConnections.Add(typeof(AbpAuditLoggingDbContext));
});
});
}
}
{
"ConnectionStrings": {
"Default": "Server=.;Database=MainDb;Trusted_Connection=True;",
"AuditLogging": "Server=.;Database=AuditDb;Trusted_Connection=True;"
}
}
Configure<AbpAuditingOptions>(options =>
{
options.IsEnabled = true;
options.EntityHistorySelectors.AddAllEntities();
// etc.
});
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
app.UseAuditing(); // required
}
Why no custom IAuditingStore is required:
Optional: schema customization
AbpAuditLoggingDbProperties.DbSchema = "audit";
Ensure your migrations align with the chosen schema.
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
What happened
Fix it step by step
Reproduce the exact migration command locally to see the full error
If you keep two DbContext model-creating extension classes, merge into one
Validate the entity property types and constraints
Ensure references used by dotnet ef build are correct
If you want to generate without DB migration to proceed
After you can run dotnet ef successfully
Notes aligning with ABP conventions
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
This is a known naming quirk in some ABP CLI template versions where the generated Blazor Server project omits the dot between the solution name and the “Blazor*” suffix for a few files. It does not affect runtime, but you can safely fix the names to match the namespace and other files.
Recommended fix (one-time, manual rename):
If you prefer to regenerate:
After renaming:
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.