Root cause: A known NullReferenceException occurs on the Users page when clicking entity actions (like Sessions or View Details) if the corresponding modal/component references are not initialized in Blazor Web App (Interactive Server/Auto). In ABP 10.0 (Identity Pro Blazor), the action handlers expect their modal components to be present and referenced. If those components are missing/not rendered (e.g., conditional rendering or missing @ref), the backing fields like SessionsModal or ViewDetailsModal are null and actions throw.
Fix: Ensure the required modals/components are in the page markup and their references are assigned before actions run.
Concrete example for a customized Users page (pattern you can adopt for Sessions/View Details too):
@using Volo.Abp.PermissionManagement.Blazor.Components
<MudDataGrid T="IdentityUserDto" @ref="_dataGrid" ServerData="LoadServerData">
<!-- ...columns... -->
<Columns>
<MudBlazor.Column T="IdentityUserDto" Field="@nameof(IdentityUserDto.Id)" Title="@L["Actions"]">
<CellTemplate>
<MudIconButton Icon="fas fa-user-lock"
OnClick="@(async (_) => await OpenPermissionsModalAsync(context.Item))"
Size="MudBlazor.Size.Small" />
<MudIconButton Icon="fas fa-list"
OnClick="@(async (_) => await OpenSessionsAsync(context.Item))"
Size="MudBlazor.Size.Small" />
<MudIconButton Icon="fas fa-eye"
OnClick="@(async (_) => await OpenViewDetailsAsync(context.Item))"
Size="MudBlazor.Size.Small" />
</CellTemplate>
</MudBlazor.Column>
</Columns>
</MudDataGrid>
<!-- Place required modals and capture refs -->
<MudPermissionManagementModal @ref="_permissionManagementModal" />
<UserSessionsModal @ref="_sessionsModal" />
<UserViewDetailsModal @ref="_viewDetailsModal" />
And in code-behind:
private MudPermissionManagementModal _permissionManagementModal;
private UserSessionsModal _sessionsModal;
private UserViewDetailsModal _viewDetailsModal;
protected virtual async Task OpenPermissionsModalAsync(IdentityUserDto entity)
{
await _permissionManagementModal.OpenDialogAsync(PermissionProviderName, entity.Id.ToString());
}
protected virtual async Task OpenSessionsAsync(IdentityUserDto entity)
{
// Ensure _sessionsModal is not null
await _sessionsModal.OpenAsync(entity.Id);
}
protected virtual async Task OpenViewDetailsAsync(IdentityUserDto entity)
{
await _viewDetailsModal.OpenAsync(entity.Id);
}
If you replaced/overrode the default Identity Users page:
For Interactive Auto/Server:
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 all of the following:
A) Keep issuer clean in Angular
oAuthConfig: {
issuer: 'https://xxxxxxxx.azurewebsites.net', // NO trailing path, NO query
redirectUri: 'https://{0}.your-domain.com',
clientId: 'YourClientId',
responseType: 'code',
scope: 'offline_access YourScope',
requireHttps: true
}
If issuer already includes /.well-known/openid-configuration or includes a query, Angular’s OIDC discovery will break.
B) Pass __tenant only to the authorize request
import { AuthService, SessionStateService } from '@abp/ng.core';
import { Injectable, inject } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class TenantAwareLogin {
private auth = inject(AuthService);
private session = inject(SessionStateService);
start() {
const t = this.session.getTenant();
const tenantParam = t?.id?.toString() ?? t?.name ?? null;
if (tenantParam) {
this.auth.navigateToLogin({ __tenant: tenantParam });
} else {
this.auth.navigateToLogin();
}
}
}
C) Avoid patching OAuth discovery If you patched OAuthService.startLogin or globally appended query params to all OAuth URLs, revert that. Only pass __tenant through AuthService.navigateToLogin extras so it reaches authorize, not discovery.
D) Auth Server (shared domain)
E) Quick checklist to eliminate the bad URL
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) Angular: force AuthGuard -> authorize request to include __tenant
Example: override AuthService.navigateToLogin to append __tenant automatically:
import { Injectable, inject } from '@angular/core';
import { AuthService, SessionStateService } from '@abp/ng.core';
@Injectable({ providedIn: 'root' })
export class TenantAwareAuthService {
private auth = inject(AuthService);
private session = inject(SessionStateService);
navigateToLogin(extras?: Record<string, unknown>) {
const t = this.session.getTenant();
const tenantParam = t?.id?.toString() ?? t?.name ?? null;
const qp = { ...(extras ?? {}) };
if (tenantParam && !qp['__tenant']) {
qp['__tenant'] = tenantParam;
}
this.auth.navigateToLogin(qp);
}
}
Then register a provider so guards use your service:
import { APP_INITIALIZER, inject } from '@angular/core';
import { OAuthService } from '@abp/ng.oauth';
import { Router } from '@angular/router';
import { TenantAwareAuthService } from './tenant-aware-auth.service';
export function patchOAuthLoginRedirect() {
return () => {
const oAuth = inject(OAuthService) as any;
const tenantAuth = inject(TenantAwareAuthService);
// Patch startLogin to ensure __tenant is sent for any login attempt
const original = oAuth.startLogin?.bind(oAuth) ?? null;
if (original) {
oAuth.startLogin = (options?: any) => {
tenantAuth.navigateToLogin(options?.params);
};
}
};
}
@NgModule({
// ...
providers: [
{ provide: APP_INITIALIZER, useFactory: patchOAuthLoginRedirect, multi: true },
],
})
export class AppModule {}
Notes:
B) Environment configuration
const baseUrl = 'https://{0}.domain.com'; // same Azure app, custom domains per tenant
export const environment = {
production: true,
application: { baseUrl, name: 'MyApp' },
oAuthConfig: {
issuer: 'https://azurehosted-domain.com', // shared Auth Server
redirectUri: baseUrl, // per-tenant SPA domain
clientId: 'MyApp_App',
responseType: 'code',
scope: 'offline_access MyApp',
requireHttps: true
},
apis: {
default: { url: 'https://api.domain.com', rootNamespace: 'MyApp' }
}
};
C) Auth Server configuration (shared domain)
{
"IdentityServer": {
"Clients": {
"MyApp_App": {
"ClientId": "MyApp_App",
"ClientSecret": "1q2w3e*",
"RootUrl": "https://{0}.domain.com"
}
}
}
}
D) API/Web Host
E) Result
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.
Configure the flow so Angular determines the tenant from subdomain, delays auto-login until tenant is resolved, and passes the tenant to the Auth Server on navigation.
A) Angular: make the first route public and pass __tenant on login
const routes: Routes = [
{ path: '', component: HomeComponent }, // public entry, no auth guard
{ path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuard] },
{ path: '**', redirectTo: '' }
];
import { AuthService, SessionStateService } from '@abp/ng.core';
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
@Component({ selector: 'app-home', templateUrl: './home.component.html' })
export class HomeComponent {
private auth = inject(AuthService);
private session = inject(SessionStateService);
private router = inject(Router);
get hasLoggedIn() { return this.auth.isAuthenticated; }
ngOnInit() {
// If already logged in, go to dashboard; otherwise show a Login button in the template
if (this.hasLoggedIn) {
this.router.navigate(['/dashboard']);
}
}
login() {
const t = this.session.getTenant();
const tenantParam = t?.id?.toString() ?? t?.name ?? null;
// Pass __tenant only when using a shared auth domain
tenantParam
? this.auth.navigateToLogin({ __tenant: tenantParam })
: this.auth.navigateToLogin();
}
}
const baseUrl = 'https://{0}.domain.com';
export const environment = {
production: true,
application: { baseUrl, name: 'MyApp' },
oAuthConfig: {
issuer: 'https://azurehosted-domain.com', // shared Auth Server
redirectUri: baseUrl, // per-tenant SPA domain
clientId: 'MyApp_App',
responseType: 'code',
scope: 'offline_access MyApp',
requireHttps: true
},
apis: {
default: { url: 'https://api.domain.com', rootNamespace: 'MyApp' }
}
};
Notes:
B) Auth Server: shared domain, no domain resolver, allow per-tenant redirect URIs
{
"IdentityServer": {
"Clients": {
"MyApp_App": {
"ClientId": "MyApp_App",
"ClientSecret": "1q2w3e*",
"RootUrl": "https://{0}.domain.com"
}
}
}
}
C) API/Web Host: domain tenant resolver if API is tenant-qualified; otherwise none
Configure<AbpTenantResolveOptions>(options =>
{
options.AddDomainTenantResolver("{0}.api.domain.com");
});
D) Fix the redirect loop and “logging into host” symptoms
E) Summary of what to change in your 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.
A) Backend (Auth Server and Web Host/API)
using Volo.Abp.MultiTenancy;
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
// Strongly recommended for wildcard tenants
context.Services.AddAbpStrictRedirectUriValidator();
context.Services.AddAbpClientConfigurationValidator();
context.Services.AddAbpWildcardSubdomainCorsPolicyService();
Configure<AbpTenantResolveOptions>(options =>
{
// For Web Host/API if tenants are like tenant1.domain.com calling the API at api.domain.com
// If your API domain is api.domain.com and NOT tenant-qualified, do NOT add tenant resolver here.
// If your API domain is tenant-qualified (tenant1.api.domain.com), add it:
// options.AddDomainTenantResolver("{0}.api.domain.com");
// For Auth Server if you want tenant-qualified issuer/login page (only when using per-tenant auth host, e.g. tenant1.auth.domain.com)
// options.AddDomainTenantResolver("{0}.auth.domain.com");
});
}
If you use a single shared Auth Server domain (auth.domain.com) for all tenants:
If you use per-tenant Auth Server subdomains (tenant1.auth.domain.com):
using Volo.Abp.OpenIddict.WildcardDomains;
public override void PreConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
PreConfigure<AbpOpenIddictWildcardDomainOptions>(options =>
{
options.EnableWildcardDomainSupport = true;
// Use your actual format
options.WildcardDomainsFormat.Add("https://{0}.auth.domain.com");
// Optionally also add SPA and API formats if needed by your setup:
// options.WildcardDomainsFormat.Add("https://{0}.domain.com");
// options.WildcardDomainsFormat.Add("https://{0}.api.domain.com");
});
}
// In API/Web Host JWT bearer configuration
services.AddAuthentication().AddJwtBearer(options =>
{
options.TokenValidationParameters.IssuerValidator =
TokenWildcardIssuerValidator.IssuerValidator;
options.TokenValidationParameters.ValidIssuers = new[]
{
"https://{0}.auth.domain.com/"
};
});
This is only needed if your API validates tokens issued by many per-tenant issuer URLs.
{
"IdentityServer": {
"Clients": {
"MyApp_App": {
"ClientId": "MyApp_App",
"ClientSecret": "1q2w3e*",
"RootUrl": "https://{0}.domain.com"
}
}
}
}
B) Frontend (Angular)
const baseUrl = 'https://{0}.domain.com';
export const environment = {
production: true,
application: {
baseUrl,
name: 'MyApp',
},
oAuthConfig: {
issuer: 'https://auth.domain.com', // Shared Auth Server domain
redirectUri: baseUrl, // per-tenant SPA domain
clientId: 'MyApp_App',
responseType: 'code',
scope: 'offline_access MyApp',
requireHttps: true
},
apis: {
default: {
url: 'https://api.domain.com', // shared API domain or tenant-qualified if you use that model
rootNamespace: 'MyApp'
}
}
};
import { AuthService, SessionStateService } from '@abp/ng.core';
import { Component, inject } from '@angular/core';
@Component({ selector: 'app-home', templateUrl: './home.component.html' })
export class HomeComponent {
private authService = inject(AuthService);
private session = inject(SessionStateService);
login() {
const tenant = this.session.getTenant();
// Prefer tenant id if available; __tenant accepts id or name
const tenantParam = tenant?.id?.toString() ?? tenant?.name ?? null;
if (tenantParam) {
this.authService.navigateToLogin({ __tenant: tenantParam });
} else {
this.authService.navigateToLogin();
}
}
}
This is the recommended approach when your Auth Server is not tenant-qualified (auth.domain.com).
C) Common causes of redirect loops or logging into host
D) Which topology is supported?
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) Prefer a Commercial Base or Convert the Solution
B) If You Must Integrate Pro Modules Into an Existing Open-Source Solution
Remove Basic counterparts before adding Pro:
Align module dependencies in each layer:
[DependsOn(
typeof(AbpAutofacModule),
typeof(AbpAspNetCoreMvcModule),
typeof(AbpIdentityHttpApiModule), // if using Identity.Pro, ensure the Pro HttpApi module is referenced
typeof(AccountAdminHttpApiModule) // Account Admin Pro HttpApi module
)]
public class MyProjectHttpApiModule : AbpModule
{
}
Clean up duplicated localization resources:
Keep package versions consistent:
Add the correct Admin/Public packages for the UI scenario:
Use ABP Suite/Studio when possible:
C) Known Compatibility Considerations and Workarounds
D) Recommended Dependency Structure (high-level)
E) Upgrades and Patches
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.
Fix it with the following steps:
A) Ensure email sending is enabled in Production
public override void ConfigureServices(ServiceConfigurationContext context)
{
#if DEBUG
context.Services.Replace(ServiceDescriptor.Singleton<IEmailSender, NullEmailSender>());
#endif
}
"Settings": {
"Abp.Mailing.Smtp.Host": "smtp.yourprovider.com",
"Abp.Mailing.Smtp.Port": "587",
"Abp.Mailing.Smtp.UserName": "user",
"Abp.Mailing.Smtp.Password": "<ENCRYPTED_PASSWORD>",
"Abp.Mailing.Smtp.EnableSsl": "true",
"Abp.Mailing.Smtp.UseDefaultCredentials": "false",
"Abp.Mailing.DefaultFromAddress": "no-reply@yourdomain.com",
"Abp.Mailing.DefaultFromDisplayName": "Your App"
}
Note: The SMTP password must be stored encrypted if you put it in settings. Use IStringEncryptionService to encrypt before saving to the DB or write a small snippet at startup to call SettingManager.SetGlobalAsync for the password (ABP encrypts on set, decrypts on get).
B) Stop loading templates from the physical file system; embed and use ABP VFS + ITemplateRenderer Right now, LoadTemplate reads files via File.ReadAllTextAsync from AppContext.BaseDirectory/Emailing/Templates. This commonly breaks in containers, single-file publish, or when paths differ.
Use ABP’s text templating + virtual file system:
[DependsOn(typeof(AbpEmailingModule))]
public class YourProjectDomainModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<YourProjectDomainModule>();
});
}
}
public static class MyEmailTemplates
{
public const string UserCreation = "MyEmailTemplates.UserCreation";
public const string ChangePassword = "MyEmailTemplates.ChangePassword";
}
public class EmailTemplateDefinitionProvider : TemplateDefinitionProvider
{
public override void Define(ITemplateDefinitionContext context)
{
context.Add(
new TemplateDefinition(MyEmailTemplates.UserCreation)
.WithVirtualFilePath("/Emailing/Templates/UserCreation.tpl", isInlineLocalized: true)
);
context.Add(
new TemplateDefinition(MyEmailTemplates.ChangePassword)
.WithVirtualFilePath("/Emailing/Templates/ChangePassword.tpl", isInlineLocalized: true)
);
}
}
private async Task<string> RenderEmailBodyAsync(string templateName, object model)
{
// Map friendly names to your template keys
var templateKey = templateName.Equals("UserCreation.tpl", StringComparison.OrdinalIgnoreCase)
? MyEmailTemplates.UserCreation
: MyEmailTemplates.ChangePassword;
return await _templateRenderer.RenderAsync(templateKey, model);
}
C) If you want to keep raw HTML templates and simple Replace (without Scriban)
private readonly IFileProvider _fileProvider;
public SendGridEmailNotificationAppServiceBase(..., IVirtualFileProvider vfs, ...)
{
_fileProvider = vfs;
}
private async Task<string> LoadTemplateFromVfsAsync(string virtualPath)
{
var file = _fileProvider.GetFileInfo(virtualPath); // e.g. "/Emailing/Templates/UserCreation.tpl"
if (!file.Exists) throw new FileNotFoundException($"Template not found: {virtualPath}");
using var stream = file.CreateReadStream();
using var reader = new StreamReader(stream);
return await reader.ReadToEndAsync();
}
D) Ensure environment-specific URLs are configured
E) Validate multi-tenancy handling and encryption
F) Use MailKit in production (recommended) If you see warnings about SmtpClient, add AbpMailKit and configure MailKit sender:
G) Quick checklist for deployed environment
Code summary (minimal changes to your class):
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 OrderPaidEvent : EntityEventData<Order>
{
public OrderPaidEvent(Order entity) : base(entity) {}
}
- Event handler to grant access:
public class OrderPaidEventHandler : ILocalEventHandler<OrderPaidEvent>, ITransientDependency
{
private readonly IRepository<Enrollment, Guid> _enrollmentRepo;
public OrderPaidEventHandler(IRepository<Enrollment, Guid> enrollmentRepo)
{
_enrollmentRepo = enrollmentRepo;
}
public async Task HandleEventAsync(OrderPaidEvent eventData)
{
var order = eventData.Entity;
foreach (var item in order.Items)
{
await _enrollmentRepo.InsertAsync(new Enrollment(order.UserId, item.CourseId), autoSave: true);
}
}
}
- Guarding course content:
public class CourseAppService : ApplicationService
{
private readonly IRepository<Enrollment, Guid> _enrollmentRepo;
private readonly IRepository<Course, Guid> _courseRepo;
public async Task<CourseDto> GetAsync(Guid courseId)
{
var isEnrolled = await _enrollmentRepo.AnyAsync(e => e.UserId == CurrentUser.GetId() && e.CourseId == courseId);
if (!isEnrolled) throw new AbpAuthorizationException("Not enrolled");
var course = await _courseRepo.GetAsync(courseId);
return ObjectMapper.Map<Course, CourseDto>(course);
}
}
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.
[DependsOn(
typeof(MyDocApplicationModule),
typeof(MyDocHttpApiModule),
typeof(MyDocEntityFrameworkCoreModule)
)]
public class HostAppHttpApiHostModule : AbpModule
{
}
4) Add the module’s EF Core Configure call into each host’s migrations DbContext (or migrations project) so the host’s DbMigrator migrates the module tables:
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ConfigureMyDoc(); // your module's extension method
}
5) Run each host’s DbMigrator to create/update databases.
This is the recommended way to reuse functionality across multiple ABP applications (module as a package rather than project reference), keeping hosts loosely coupled.
Generating Angular proxies without ABP Suite entities
Database segregation approaches when reusing the same module in two hosts
Configure<AbpDbContextOptions>(options =>
{
options.Configure<MyDocDbContext>(opt => { opt.UseSqlServer(); /* or UseMySQL etc. */ });
});
- This allows Host 1 and Host 2 to point the module to different databases or even different DBMS per host while sharing the same binary module. See also the note below for multiple providers per solution.
Deployment separation
Notes and tips
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.
Use the correct bundling modules on the host (server) project
Example:
[DependsOn(
typeof(AbpAutofacModule),
typeof(AbpAspNetCoreMvcUiBundlingModule),
typeof(AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeBundlingModule)
// + any commercial WebAssembly bundling modules you use, e.g.:
// typeof(AbpAuditLoggingBlazorWebAssemblyBundlingModule)
)]
public class MyProjectNameBlazorModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
// Add services to the container
context.Services.AddRazorComponents()
.AddInteractiveWebAssemblyComponents();
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
var env = context.GetEnvironment();
if (env.IsDevelopment())
{
app.UseWebAssemblyDebugging();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
// IMPORTANT: maps static/virtual assets including global.js & global.css
app.MapAbpStaticAssets();
app.UseRouting();
app.UseAntiforgery();
app.UseConfiguredEndpoints(builder =>
{
builder.MapRazorComponents<App>()
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(WebAppAdditionalAssembliesHelper.GetAssemblies<MyProjectNameBlazorClientModule>());
});
}
}
public class MonacoBundleScriptContributor : BundleContributor
{
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddIfNotContains("_content/BlazorMonaco/jsInterop.js");
context.Files.AddIfNotContains("_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.js");
context.Files.AddIfNotContains("_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.js");
}
}
// In the same Blazor.Client project (e.g., in your module)
private void ConfigureBundles()
{
Configure<AbpBundlingOptions>(options =>
{
var globalScripts = options.ScriptBundles.Get(BlazorWebAssemblyStandardBundles.Scripts.Global);
globalScripts.AddContributors(typeof(MonacoBundleScriptContributor));
});
}
Notes:
Use AddIfNotContains and relative paths starting with _content/ for library static files.
Ensure the Blazor.Client project references the BlazorMonaco package so those _content files exist at runtime.
Reference the client project from the host project
Ensure MapAbpStaticAssets is called in the host pipeline
Verify at runtime
Common pitfalls to check
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.