Cause
Fix/Workarounds
Cause
Checklist to fix
Project: MeinTestDemoWerk.ProcessService.Domain
Folder: Processes
File example: using Volo.Abp.Domain.Entities.Auditing;
namespace MeinTestDemoWerk.ProcessService.Processes { public class Process : FullAuditedAggregateRoot<Guid> { public string Name { get; set; } // other props... } }
Notes
If issues persist
Sources
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.
Key steps:
Keep API contracts identical
Provide two hosts with the same API surface
[DependsOn(
typeof(MyProjectApplicationModule),
typeof(MyProjectHttpApiModule),
typeof(AbpAspNetCoreMvcModule)
)]
public class MyProjectLocalHostModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
// Configure EF Core to SQLite for local store
Configure<AbpDbConnectionOptions>(options =>
{
options.ConnectionStrings.Default = "Data Source=local.db";
});
}
}
public override void PreConfigureServices(ServiceConfigurationContext context)
{
PreConfigure<AbpRemoteServiceOptions>(options =>
{
var isOffline = /* your connectivity check */;
options.RemoteServices.Default = new RemoteServiceConfiguration(
baseUrl: isOffline ? "https://localhost:5005" : "https://central-host.example.com"
);
});
}
Optionally, add Polly retry logic to smooth transitions.
Use a local EF Core provider
Authentication considerations
Data synchronization (when back online)
Caching and queues
Blazor hosting choice
Minimal local host example
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// SQLite connection for local mode
builder.Services.Configure<AbpDbConnectionOptions>(options =>
{
options.ConnectionStrings.Default = builder.Configuration.GetConnectionString("Default");
});
await builder.AddApplicationAsync<MyProjectLocalHostModule>();
var app = builder.Build();
await app.InitializeApplicationAsync();
app.MapControllers();
await app.RunAsync();
}
}
appsettings.json (local): { "ConnectionStrings": { "Default": "Data Source=local.db" }, "App": { "SelfUrl": "https://localhost:5005" } }
What ABP gives you vs what you must implement
This approach aligns with common ABP conventions, but specific official documentation for an “offline local middleware with identical API + sync back to central SQL Server” is not available.
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.
A) Backend (HttpApi.Host) – Resolve tenant from subdomain and allow wildcard OpenIddict/redirects
// using Volo.Abp.OpenIddict.WildcardDomains;
PreConfigure<AbpOpenIddictWildcardDomainOptions>(options =>
{
options.EnableWildcardDomainSupport = true;
// Add all authority formats that will be used by the auth server (API host if combined)
// Example for environments:
options.WildcardDomainsFormat.Add("https://{0}.app-qa.example.com");
options.WildcardDomainsFormat.Add("https://{0}.app-pre.example.com");
options.WildcardDomainsFormat.Add("https://{0}.app.example.com");
// If your API host is also subdomained (like {0}.api.app-qa.example.com), add those too.
});
// using Volo.Abp.MultiTenancy;
Configure<AbpTenantResolveOptions>(options =>
{
// Adjust formats to your real hostnames used for API endpoints receiving requests.
options.AddDomainTenantResolver("{0}.app-qa.example.com");
options.AddDomainTenantResolver("{0}.app-pre.example.com");
options.AddDomainTenantResolver("{0}.app.example.com");
// If your API host has a different subdomain pattern, add its formats as well.
});
// using Owl.TokenWildcardIssuerValidator;
context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = configuration["AuthServer:Authority"]; // Your base authority
options.RequireHttpsMetadata = true;
// Allow wildcard issuers for subdomain tenants
options.TokenValidationParameters.IssuerValidator = TokenWildcardIssuerValidator.IssuerValidator;
options.TokenValidationParameters.ValidIssuers = new[]
{
"https://{0}.app-qa.example.com/",
"https://{0}.app-pre.example.com/",
"https://{0}.app.example.com/"
};
});
context.Services.AddAbpStrictRedirectUriValidator();
context.Services.AddAbpClientConfigurationValidator();
context.Services.AddAbpWildcardSubdomainCorsPolicyService();
"IdentityServer": {
"Clients": {
"My_Blazor_Wasm": {
"ClientId": "My_Blazor_Wasm",
"ClientSecret": "1q2w3e*",
"RootUrl": "https://{0}.app-qa.example.com"
}
}
}
Repeat entries for each environment if you keep per-env DBs; otherwise include each environment’s RootUrl pattern if you use one central auth DB for all envs. Re-run DbMigrator so allowed redirect URIs/CORS origins are updated.
B) Blazor Interactive (WebAssembly-only) – Propagate the current tenant’s subdomain to Authority and BaseUrl
For Blazor WebAssembly, there is no server-side tenant resolver in the UI process. You must dynamically shape the Authority and BaseUrl at startup using the current browser URL. This is documented for Angular and demonstrated for Blazor in community samples. Implement the pattern below:
Example utility methods:
private static readonly string[] ProtocolPrefixes = { "http://", "https://" };
private static string ConvertToTenantSubDomain(WebAssemblyHostBuilder builder, string configPath)
{
var baseUrl = builder.HostEnvironment.BaseAddress; // e.g., https://tenant1.app-qa.example.com/
var configUrl = builder.Configuration[configPath]; // e.g., "https://{0}.api.app-qa.example.com"
return configUrl.Replace("{0}.", GetTenantNamePrefix(baseUrl));
}
// Returns "tenant1." or "" when no tenant
private static string GetTenantNamePrefix(string baseUrl)
{
var hostName = baseUrl.RemovePreFix(ProtocolPrefixes); // tenant1.app-qa.example.com/
var host = hostName.TrimEnd('/').Split('/')[0]; // tenant1.app-qa.example.com
var parts = host.Split('.');
// Your rule: "tenant-env.example.com" => first label is tenant, second is env-app
// E.g., tenant1.app-qa.example.com => tenant is parts[0], rest identifies environment/app
return parts.Length > 2 ? $"{parts[0]}." : string.Empty;
}
builder.Services.AddOidcAuthentication(options =>
{
builder.Configuration.Bind("AuthServer", options.ProviderOptions);
options.UserOptions.RoleClaim = "role";
options.ProviderOptions.Authority = ConvertToTenantSubDomain(builder, "AuthServer:Authority");
});
Configure<AbpRemoteServiceOptions>(options =>
{
options.RemoteServices.Default = new RemoteServiceConfiguration(
ConvertToTenantSubDomain(builder, "RemoteServices:Default:BaseUrl")
);
});
{
"App": {
"SelfUrl": "https://{0}.app-qa.example.com"
},
"AuthServer": {
"Authority": "https://{0}.api.app-qa.example.com",
"ClientId": "My_Blazor_Wasm",
"ResponseType": "code"
},
"RemoteServices": {
"Default": {
"BaseUrl": "https://{0}.api.app-qa.example.com"
}
}
}
Repeat per environment or compute environment from the current host if you prefer a single file with multiple candidates.
C) Cookie/Authentication isolation across tenants in WASM If you observe “login to one tenant logs me into all,” your auth cookie is likely scoped to the parent domain. Always let the auth cookie domain be the exact host (no leading dot, Domain = null). OpenIddict server defaults typically align with this; avoid setting a shared cookie domain. Clear browser cookies after changes.
D) Resolver order and middleware ABP’s default resolver order starts with CurrentUserTenantResolveContributor, then QueryString, Route, Header, Cookie. Adding the domain resolver (as shown) integrates correctly. Ensure app.UseAbpRequestLocalization(), app.UseRouting(), app.UseAuthentication(), app.UseAbpMultiTenancy(), app.UseAuthorization() are in the standard order generated by templates.
E) Environment-based subdomains (app-qa/app-pre/app)
F) Known limitations and notes for Blazor WASM
G) Minimal verification steps
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.
Swagger succeeds because it automatically sends the required headers (Authorization, X-Requested-With, RequestVerificationToken, etc.). Your browser/web client likely does not. That often results in a 404 Not Found (ABP hides some auth-related responses to avoid information leakage) unless you send the expected headers/tokens.
Checklist to fix the Not Found when calling from Web:
Ensure you send Authorization like Swagger does
Include the ABP Ajax header to avoid anti-forgery validation (GET is safe, but ABP expects the Ajax header in many UI scenarios)
If you enabled Anti-Forgery (default for MVC/Razor), either:
Confirm your frontend is calling the exact same URL and method
If your app is multi-tenant and the request needs tenant context
Ensure route precedence is not overridden
Test quickly with curl or Postman using the same headers Swagger shows
Example: minimal safe controller annotations if you don’t want anti-forgery for GET
[Authorize(KSVRPermissions.VehicleOwners.Default)]
[Route("api/ksvr/vehicle-owners")]
public class VehicleOwnersAppService : ApplicationService
{
[IgnoreAntiforgeryToken] // optional if you face CSRF checks in your UI pipeline
[HttpGet("lookup/autocomplete")]
public virtual async Task<ListResultDto<VehicleAutocompleteDto>> AutocompleteAsync([FromQuery] string keyword)
{
...
}
}
And sample JS fetch with the required headers:
const res = await fetch('http://localhost:44303/api/ksvr/vehicle-owners/lookup/autocomplete?keyword=29', {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
'X-Requested-With': 'XMLHttpRequest'
// 'RequestVerificationToken': csrfToken // if needed in your setup
// 'Abp-Tenant': tenantName // if multi-tenant by name
},
credentials: 'include'
});
Why your other API works while autocomplete doesn’t
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.
Problem recap: Each tenant has a completely different domain (no common base), and each tenant must use its own OIDC ClientId and its own authority/redirect/logout URLs. You’re on ABP 10.0.0, Blazor Web App, Auth server embedded in your API. You also don’t use the tenant cookie resolver.
Key points supported by ABP:
Step-by-step
A) Resolve tenant by arbitrary domain (no shared base):
using Volo.Abp.MultiTenancy;
using Microsoft.AspNetCore.Http;
public class CustomDomainTenantResolveContributor : TenantResolveContributorBase
{
public const string ContributorName = "CustomDomain";
public override string Name => ContributorName;
protected override Task<string?> GetTenantIdOrNameFromHttpContextOrNullAsync(
ITenantResolveContext context, HttpContext httpContext)
{
var host = httpContext.Request.Host.Host.ToLowerInvariant();
// Map arbitrary domains to tenant names
// Example mappings; move to DB/config as needed
return Task.FromResult(host switch
{
"contoso-portal.com" => "contoso",
"fabrikam-portal.net" => "fabrikam",
"tailspinsolutions.io" => "tailspin",
_ => null
});
}
}
public class YourHttpApiHostModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpTenantResolveOptions>(opts =>
{
// Optional: keep any domain formats you can express
// opts.AddDomainTenantResolver("{0}.example.com");
// Add custom mapping contributor at the beginning
opts.TenantResolvers.Insert(0, new CustomDomainTenantResolveContributor());
});
}
}
B) Store and use per-tenant OIDC client settings (ClientId, redirect/logout URLs):
public class MyOpenIddictSeeder : ITransientDependency
{
private readonly ICurrentTenant _currentTenant;
private readonly IOpenIddictApplicationManager _appManager;
public MyOpenIddictSeeder(ICurrentTenant currentTenant, IOpenIddictApplicationManager appManager)
{
_currentTenant = currentTenant;
_appManager = appManager;
}
public async Task SeedAsync()
{
using (_currentTenant.Change("contoso")) // repeat for each tenant, or loop from a store
{
var clientId = "contoso_blazor";
var redirectUri = "https://contoso-portal.com/signin-oidc";
var postLogoutRedirectUri = "https://contoso-portal.com/signout-callback-oidc";
var corsOrigin = "https://contoso-portal.com";
if (await _appManager.FindByClientIdAsync(clientId) == null)
{
await _appManager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = clientId,
DisplayName = "Contoso Blazor",
Type = OpenIddictConstants.ClientTypes.Public,
ConsentType = OpenIddictConstants.ConsentTypes.Implicit,
RedirectUris = { new Uri(redirectUri) },
PostLogoutRedirectUris = { new Uri(postLogoutRedirectUri) },
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Authorization,
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.Endpoints.Logout,
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
OpenIddictConstants.Permissions.ResponseTypes.Code,
OpenIddictConstants.Permissions.Scopes.Email,
OpenIddictConstants.Permissions.Scopes.Profile,
OpenIddictConstants.Permissions.Scopes.Roles
},
Requirements = { OpenIddictConstants.Requirements.Features.ProofKeyForCodeExchange }
});
// If you use ABP helper validators for client config, also register CORS origins appropriately.
}
}
// Repeat for each tenant with its own exact domains/URIs.
}
}
C) Make the Blazor Web App use the tenant’s client and authority dynamically
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Volo.Abp.MultiTenancy;
public class TenantOidcOptionsConfigurator : IConfigureNamedOptions<OpenIdConnectOptions>
{
private readonly IConfiguration _cfg;
private readonly ICurrentTenant _currentTenant;
public TenantOidcOptionsConfigurator(IConfiguration cfg, ICurrentTenant currentTenant)
{
_cfg = cfg;
_currentTenant = currentTenant;
}
public void Configure(string name, OpenIdConnectOptions options)
{
if (name != "oidc") return;
var map = _cfg.GetSection("TenantOidc").Get<Dictionary<string, TenantOidcConfig>>();
// TenantOidc: { "contoso": { "Authority": "https://api.contoso.com", "ClientId": "contoso_blazor" }, ... }
var tenantName = _currentTenant.Name;
if (!tenantName.IsNullOrWhiteSpace() && map != null && map.TryGetValue(tenantName!, out var t))
{
options.Authority = t.Authority; // must match issuer
options.ClientId = t.ClientId;
var prevLogin = options.Events.OnRedirectToIdentityProvider;
options.Events.OnRedirectToIdentityProvider = async ctx =>
{
ctx.ProtocolMessage.IssuerAddress = options.Authority!.EnsureEndsWith('/') + "connect/authorize";
if (prevLogin != null) await prevLogin(ctx);
};
var prevLogout = options.Events.OnRedirectToIdentityProviderForSignOut;
options.Events.OnRedirectToIdentityProviderForSignOut = async ctx =>
{
ctx.ProtocolMessage.IssuerAddress = options.Authority!.EnsureEndsWith('/') + "connect/logout";
if (prevLogout != null) await prevLogout(ctx);
};
}
}
public void Configure(OpenIdConnectOptions options) => Configure(Options.DefaultName, options);
}
public class TenantOidcConfig
{
public string Authority { get; set; }
public string ClientId { get; set; }
}
context.Services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, TenantOidcOptionsConfigurator>();
{
"TenantOidc": {
"contoso": {
"Authority": "https://api.contoso.com",
"ClientId": "contoso_blazor"
},
"fabrikam": {
"Authority": "https://api.fabrikam.net",
"ClientId": "fabrikam_blazor"
}
}
}
builder.Services.AddOidcAuthentication(options =>
{
var cfg = builder.Configuration.GetSection("TenantOidc").GetChildren()
.ToDictionary(x => x.Key, x => new { Authority = x["Authority"], ClientId = x["ClientId"] });
var host = new Uri(builder.HostEnvironment.BaseAddress).Host.ToLowerInvariant();
// Map host -> tenant key, you can keep a map section HostToTenant in config
var hostMap = builder.Configuration.GetSection("HostToTenant").Get<Dictionary<string,string>>();
if (hostMap != null && hostMap.TryGetValue(host, out var tenantKey) && cfg.TryGetValue(tenantKey, out var t))
{
options.ProviderOptions.Authority = t.Authority;
options.ProviderOptions.ClientId = t.ClientId;
}
options.ProviderOptions.ResponseType = "code";
});
{
"TenantOidc": {
"contoso": { "Authority": "https://api.contoso.com", "ClientId": "contoso_wasm" },
"fabrikam": { "Authority": "https://api.fabrikam.net", "ClientId": "fabrikam_wasm" }
},
"HostToTenant": {
"contoso-portal.com": "contoso",
"fabrikam-portal.net": "fabrikam"
}
}
D) Token validation in API when multiple issuers exist
context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = true;
// If each tenant has a different issuer:
options.TokenValidationParameters.ValidIssuers = new[]
{
"https://api.contoso.com/",
"https://api.fabrikam.net/"
};
// Or implement a custom IssuerValidator to check against a store of tenant issuers.
});
E) Disable tenant switching UI on AuthServer
F) Summary checklist for your scenario (unique domains)
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.
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.