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.
Key points
Step-by-step
A) Install and wire modules
/Payment/GatewaySelection/Payment/Stripe/PrePaymentB) Configure Payment and SaaS
"Payment": {
"Stripe": {
"PublishableKey": "pk_test_xxx",
"SecretKey": "sk_test_xxx",
"WebhookSecret": "whsec_xxx",
"Currency": "USD", // set as needed
"Locale": "auto",
"PaymentMethodTypes": [] // leave empty; module adds “card” automatically
}
}
public override void PreConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
Configure<PaymentWebOptions>(options =>
{
options.RootUrl = configuration["App:SelfUrl"];
options.CallbackUrl = configuration["App:SelfUrl"] + "/PaymentSucceed";
});
}
C) Define SaaS Editions, Plans and Stripe mapping
D) Self-service “change plan” UI flow (tenant-facing)
Example MVC page model (tenant UI):
public class ChangePlanModel : PageModel
{
private readonly IPaymentRequestAppService _paymentRequestAppService;
private readonly ICurrentTenant _currentTenant;
public ChangePlanModel(IPaymentRequestAppService paymentRequestAppService, ICurrentTenant currentTenant)
{
_paymentRequestAppService = paymentRequestAppService;
_currentTenant = currentTenant;
}
public async Task<IActionResult> OnPostAsync(Guid editionId, Guid planId)
{
// Carry needed info for webhook/finalization
var pr = await _paymentRequestAppService.CreateAsync(
new PaymentRequestCreateDto
{
Products =
{
new PaymentRequestProductCreateDto
{
PaymentType = PaymentType.Subscription,
Name = "PlanChange",
Code = $"{_currentTenant.Id}_{planId}",
Count = 1,
PlanId = planId
}
}
}.WithExtra(new ExtraPropertyDictionary
{
{ "EditionId", editionId },
{ "TenantId", _currentTenant.Id }
})
);
return LocalRedirectPreserveMethod("/Payment/GatewaySelection?paymentRequestId=" + pr.Id);
}
}
Helper extension (optional):
public static class PaymentRequestDtoExtensions
{
public static PaymentRequestCreateDto WithExtra(this PaymentRequestCreateDto dto, ExtraPropertyDictionary extras)
{
dto.ExtraProperties = extras;
return dto;
}
}
Notes
E) Redirects and built-in UI
F) Webhooks are mandatory
G) Downgrade and upgrade are the same flow
H) If you prefer ISubscriptionAppService
I) Angular UI
Common pitfalls and fixes
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.
Do the following:
"App": {
"SelfUrl": "https://10.136.36.151:44380",
"RedirectAllowedUrls": "http://localhost:4200,http://10.136.36.151:4200,https://10.136.36.151:4200"
}
Notes:
Include every URL the SPA can use: local dev, IP:port, domain names, and HTTPS variants (if applicable).
If you terminate TLS elsewhere but serve Angular over HTTPS, include the https:// variant.
Restart the AuthServer after changing appsettings.
Ensure the OpenIddict client (NovaLab_App) has the exact redirect URIs
Align Angular environment configuration
Don’t mix schemes unintentionally
Reverse proxy/containers tip
After these changes, restart services. The login should proceed without “invalid redirect_uri”.
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 steps below:
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Bundling;
public class ProdigyEnablerWebModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpBundlingOptions>(options =>
{
options.StyleBundles.Configure(BasicThemeBundles.Styles.Global, bundle =>
{
// Path must be relative to web root (wwwroot)
bundle.AddFiles("/styles/global-styles.css");
});
});
}
}
Notes:
<abp-style-bundle name="@BasicThemeBundles.Styles.Global" />
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
var env = context.GetEnvironment();
if (!env.IsDevelopment())
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.MapAbpStaticAssets(); // important for ABP’s virtual files
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseConfiguredEndpoints();
}
Ensure client libraries and bundles are present when you build/publish:
Dockerfile (reference pattern)
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Copy solution and restore
COPY ./ ./
RUN dotnet restore
# Install ABP libs for the Web project (adjust path to your Web csproj folder)
WORKDIR /src/YourCompany.YourProj.Web
RUN dotnet tool restore
RUN dotnet tool install -g Volo.Abp.Cli || true
ENV PATH="$PATH:/root/.dotnet/tools"
RUN abp install-libs
# Build and publish
WORKDIR /src
RUN dotnet publish YourCompany.YourProj.Web/YourCompany.YourProj.Web.csproj -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
ENTRYPOINT ["dotnet", "YourCompany.YourProj.Web.dll"]
CI/CD (DigitalOcean)
Verify at runtime
Common pitfalls that cause this error
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.