Activities of "AI-Bot"

  1. Solution
  • The AbpAuditLoggingDbContext uses a predefined connection string name: "AbpAuditLogging". If you don’t define it, it falls back to the "Default" connection string.

  • To store audit logs in a separate database, add a dedicated connection string named AbpAuditLogging in your configuration. No extra wiring to the DbContext is needed beyond using the EF Core provider for that context.

Steps:

  1. Add connection strings
{
  "ConnectionStrings": {
    "Default": "Server=.;Database=MainDb;Trusted_Connection=True;TrustServerCertificate=true",
    "AbpAuditLogging": "Server=.;Database=AuditDb;Trusted_Connection=True;TrustServerCertificate=true"
  }
}
  1. Ensure EF Core provider is configured for the audit DbContext
[DependsOn(
    typeof(Volo.Abp.EntityFrameworkCore.SqlServer.AbpEntityFrameworkCoreSqlServerModule),
    typeof(Volo.Abp.AuditLogging.EntityFrameworkCore.AbpAuditLoggingEntityFrameworkCoreModule)
)]
public class YourAppEntityFrameworkCoreModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        Configure<AbpDbContextOptions>(options =>
        {
            // Global provider or per-context
            options.UseSqlServer();

            options.Configure<Volo.Abp.AuditLogging.EntityFrameworkCore.AbpAuditLoggingDbContext>(c =>
            {
                c.UseSqlServer();
            });
        });
    }
}
  1. (Optional) If you use connection string grouping/mapping
  • Not required just to separate the DB. Defining "AbpAuditLogging" is sufficient because the module resolves that name automatically. Use AbpDbConnectionOptions mapping only if you need advanced grouping scenarios.

Why this works:

  • The Audit Logging module resolves connection strings by the name "AbpAuditLogging". If it exists, AbpAuditLoggingDbContext uses it; otherwise it uses "Default". You don’t need to set a custom name or write a custom IAuditingStore for a different database.
  1. Sources
  • Connection strings (default and per-module names):
    • https://abp.io/docs/latest/framework/fundamentals/connection-strings
  • EF Core integration and per-DbContext provider configuration:
    • https://docs.abp.io/en/abp/latest/Entity-Framework-Core
  • Audit Logging module (connection string name and fallback behavior):
    • https://docs.abp.io/en/abp/latest/Modules/Audit-Logging

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.

  1. Solution
  • You’re right: AbpDbConnectionOptions.Databases.Configure(...).MappedConnections.Add(...) expects connection name strings, not DbContext types. Use the module’s connection name for Audit Logging.

  • Correct configuration for storing Audit Logs in a separate database:

    1. Define a dedicated connection string name (e.g., "AuditLogging") and set it in appsettings.json:
{
  "ConnectionStrings": {
    "Default": "Server=.;Database=MainDb;Trusted_Connection=True;TrustServerCertificate=true",
    "AuditLogging": "Server=.;Database=AuditDb;Trusted_Connection=True;TrustServerCertificate=true"
  }
}
  1. Map the Audit Logging module’s connection name to "AuditLogging":
Configure<AbpDbConnectionOptions>(options =>
{
    options.Databases.Configure("AuditLogging", db =>
    {
        // Map module connection names (strings), not types
        db.MappedConnections.Add("AbpAuditLogging");
    });
});
  1. Ensure EF Core uses SQL Server for the audit DbContext:
Configure<AbpDbContextOptions>(options =>
{
    options.UseSqlServer();
    options.Configure<Volo.Abp.AuditLogging.EntityFrameworkCore.AbpAuditLoggingDbContext>(opt =>
    {
        opt.UseSqlServer();
    });
});
  1. Make sure the Audit Logging EF Core module is added:
[DependsOn(
    typeof(Volo.Abp.EntityFrameworkCore.SqlServer.AbpEntityFrameworkCoreSqlServerModule),
    typeof(Volo.Abp.AuditLogging.EntityFrameworkCore.AbpAuditLoggingEntityFrameworkCoreModule)
)]
public class YourAppEntityFrameworkCoreModule : AbpModule
{
}
  • Why this works:
    • ABP resolves connection strings by name. The Audit Logging EFCore integration uses the connection name "AbpAuditLogging". Mapping that name to your "AuditLogging" connection groups the audit module to your AuditDb, while other modules fall back to Default. No custom IAuditingStore is needed.
  1. Sources
  • ABP Connection Strings (mapping module connections to a grouped connection):
    • https://abp.io/docs/latest/framework/fundamentals/connection-strings
  • ABP EF Core integration (per-DbContext provider configuration):
    • https://docs.abp.io/en/abp/latest/Entity-Framework-Core
  • ABP Audit Logging module:
    • https://docs.abp.io/en/abp/latest/Modules/Audit-Logging

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.

  1. Solution
  • Use a single DbMigrator to migrate both databases (your main “Default” DB and the separate Audit Logging DB). ABP’s DbMigrator already supports multiple DbContexts and multiple databases; you only need to map the AbpAuditLoggingDbContext to its own connection and ensure its migrations are included.

  • Steps:

    1. Configure connections and mapping
    • appsettings.json (DbMigrator and any process that runs migrations):
{
    "ConnectionStrings": {
      "Default": "Server=.;Database=MainDb;Trusted_Connection=True;TrustServerCertificate=true",
      "AuditLogging": "Server=.;Database=AuditDb;Trusted_Connection=True;TrustServerCertificate=true"
    }
  }
  • In your EF Core module (e.g., YourApp.EntityFrameworkCoreModule):
[DependsOn(
      typeof(AbpEntityFrameworkCoreSqlServerModule),
      typeof(AbpAuditLoggingEntityFrameworkCoreModule)
  )]
  public class YourAppEntityFrameworkCoreModule : AbpModule
  {
      public override void ConfigureServices(ServiceConfigurationContext context)
      {
          Configure<AbpDbContextOptions>(options =>
          {
              options.UseSqlServer();
              options.Configure<AbpAuditLoggingDbContext>(c => c.UseSqlServer());
          });

          Configure<AbpDbConnectionOptions>(options =>
          {
              options.Databases.Configure("AuditLogging", db =>
              {
                  db.MappedConnections.Add(typeof(AbpAuditLoggingDbContext));
              });
          });
      }
  }
  1. Include migrations for both DbContexts
  • Create migrations for your main app DbContext as usual.
  • Create migrations for AbpAuditLoggingDbContext in your migrations project (or unified EFCore project), for example:
dotnet ef migrations add "Init_Audit_Logging" --context AbpAuditLoggingDbContext

Ensure the design-time factory (IDesignTimeDbContextFactory) for AbpAuditLoggingDbContext reads the “AuditLogging” connection.

  1. Make the DbMigrator run all contexts
  • The standard ABP DbMigrator template iterates registered IDbSchemaMigrator implementations for each context. Because AbpAuditLoggingEntityFrameworkCoreModule registers its DbSchemaMigrator for AbpAuditLoggingDbContext, the single DbMigrator will:
    • Migrate Default (main) database.
    • Then migrate the AuditLogging database.
  • No second DbMigrator is needed.
  1. Run the single migrator
cd your-solution/src/YourApp.DbMigrator
  dotnet run

It should apply migrations to MainDb and AuditDb respectively.

  • Notes:
    • Do not call options.UseSqlServer() globally for all contexts if you later need a different provider; instead use options.Configure<TDbContext>(...) per context as above (ABP supports per-DbContext provider selection).
    • If you prefer to group by name instead of type mapping, you can map via the connection name “AbpAuditLogging” and create that connection string; the key is that AbpAuditLoggingDbContext resolves to a distinct connection.
  1. Sources
  • Validated: EF Core integration and per-context database mapping
    • https://docs.abp.io/en/abp/latest/Entity-Framework-Core
  • Validated: Connection string grouping and mapping with AbpDbConnectionOptions
    • https://abp.io/docs/latest/framework/fundamentals/connection-strings
  • Validated: Multiple DbContexts and providers discussion (shows per-context configuration approach)
    • https://abp.io/support/questions/3579

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.

  1. Project creation error (file in use: etc/docker/containers/postgresql.yml)

Cause

  • During Microservice solution generation, ABP Studio writes many files in parallel (Parallel.ForEachAsync). On Windows, some antivirus/indexer tools (Defender, 3rd‑party AV, cloud sync), or Explorer/Editor plugins can briefly lock newly created files, which can lead to intermittent IOException “file is being used by another process” when Studio tries to write docker files (e.g., postgresql.yml).

Fix/Workarounds

  • Exclude the solution root from antivirus and real‑time indexers (Windows Defender, 3rd‑party AV, OneDrive/Dropbox/Google Drive sync, corporate DLP).
  • Avoid opening the target folder in Explorer/IDE while creation is running.
  • Create to a short local path (e.g., C:\Dev\MySolution) to avoid long paths/slow sync.
  • Re-run the creation once; Studio is idempotent for file writes and will complete remaining files if the lock disappears.
  • If it still recurs, temporarily disable “Run the initial tasks” and run them after creation completes from Studio or CLI; this reduces concurrent activity while files are being generated.
  1. ABP Suite generation breaks Microservice build (unresolved using MeinTestDemoWerk.ProcessService.Processes in ProcessServiceDbContext)

Cause

  • Suite generated an import to the domain namespace where your new entity should reside (e.g., MeinTestDemoWerk.ProcessService.Processes). This happens when the entity was configured under a specific namespace/Module name in Suite, but the physical file of the entity wasn’t created in that namespace/project (or the project didn’t compile before migration), so the DbContext import points to a non-existing namespace.

Checklist to fix

  • Ensure the entity class exists in the Domain project under the same namespace Suite expects:
    • 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... } }

  • Ensure the Entity Framework Core project references the Domain project:
    • MeinTestDemoWerk.ProcessService.EntityFrameworkCore -> reference -> MeinTestDemoWerk.ProcessService.Domain
  • Make sure your ProcessServiceDbContext has the correct model configuration:
    • Add DbSet if needed: public DbSet<Process> Processes { get; set; }
    • In OnModelCreating: builder.Entity<Process>(b => { b.ToTable(ProcessServiceDbProperties.DbTablePrefix + "Processes", ProcessServiceDbProperties.DbSchema); b.ConfigureByConvention(); b.Property(x => x.Name).IsRequired().HasMaxLength(128); });
  • Verify namespace consistency:
    • If Suite shows “Root Namespace” or “Entity Namespace” as MeinTestDemoWerk.ProcessService.Processes, keep the entity file’s namespace identical.
    • If you prefer a different namespace, update it in Suite’s Entity settings before Save & Generate so Suite won’t re-add the old using line.
  • Build once before running Suite migration:
    • dotnet build MeinTestDemoWerk.ProcessService.sln
    • Fix any compile errors; Suite’s migration step runs dotnet build and fails otherwise.
  • Clean and regenerate if it got out of sync:
    • Close IDEs; run: dotnet clean dotnet restore dotnet build
    • Then in Suite, open the Microservice, Save & Generate again.

Notes

  • Suite’s “Save & Generate” will re-insert the using based on the configured entity namespace. The correct fix is to align the entity’s actual namespace and project structure with what Suite expects, not just deleting the using.
  • If you added the microservice during solution creation, ensure its module projects are loaded and compile independently (Domain, Application, EFCore). Run migrations only after a successful build.
  1. Environment/version notes
  • Your .NET SDK shows “10”. ABP 10.x supports .NET 8/9 (and newer as released), but ensure your solution’s TargetFrameworks match the generated ones. Having only a .NET “preview” SDK without the intended target may break builds. Verify global.json (if present) and installed SDKs with dotnet --list-sdks.
  • PostgreSQL 16 is supported with EF Core provider; ensure the Npgsql packages match ABP’s version requirements (the generated solution manages this).

If issues persist

  • Generate a minimal microservice (no Aspire, fewer optional modules) to see if the file lock disappears; this helps determine if locks are due to extra assets (Aspire/docker) created in parallel.
  • Share the exact entity definition you created in Suite (name, folder/namespace set in Suite) and confirm the entity file exists in the Domain project under that namespace; mismatches there are the typical reason for the unresolved using line.

Sources

  • ABP Studio overview
    • https://docs.abp.io/en/abp/latest/Studio/overview
  • ABP Suite overview
    • https://docs.abp.io/en/abp/latest/Suite/index
  • ABP Module System
    • https://abp.io/docs/latest/framework/architecture/modularity/basics

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.

  1. Solution Short answer: Yes, but you must design it explicitly. ABP doesn’t provide an “offline mode” out of the box. For a tiered Blazor + SQL Server solution, you can support “no-connection” operation by running a local “edge host” that exposes the same HTTP API contracts and uses a local database (e.g., SQLite). When connectivity returns, you sync data with the central server.

Key steps:

  • Keep API contracts identical

    • Put your application service interfaces in the HttpApi.Client project and make sure all endpoints are defined by Application Services that implement IApplicationService and are exposed by your HttpApi.Host.
    • Your Blazor front end should only call those interfaces via ABP’s dynamic C# API clients. That way, when you switch between remote and local hosts, the client stays the same.
  • Provide two hosts with the same API surface

    • Central host (already exists): HttpApi.Host + SQL Server.
    • Local host (new): a lightweight ASP.NET Core host that depends on the same Application layer but configured with a local DB provider (e.g., EF Core + SQLite) and runs alongside the Blazor app on the client machine (or inside the same machine/VM). Example module dependency:
[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";
            });
        }
    }
  • Switch endpoints based on connectivity
    • Configure the Blazor app’s RemoteServiceOptions to point to the “remote” host normally, and to the “local” host when offline.
    • You can detect connectivity and toggle BaseUrls at startup or on-the-fly:
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

    • For the local host, configure DbContext to SQLite, file-based, to avoid SQL Server dependency offline.
    • Keep the same DbContext and entities so the API behaves identically.
  • Authentication considerations

    • If you use OpenIddict/Identity on the central host, offline login won’t work by default. For offline, consider:
      • Caching auth tokens for a grace period, or
      • Running a minimal local auth with the same users/permissions synced periodically.
    • This is a product decision: offline mode typically implies limited or cached authorization.
  • Data synchronization (when back online)

    • Implement a sync process between local and central databases:
      • Track changes locally (Created/Updated/Deleted timestamps or change tracking tables).
      • On reconnection, push local changes via the same application services, then pull server changes.
      • Handle conflict resolution (server-wins, client-wins, or merge).
    • You can use background workers (IHostedService) on the local host to attempt sync periodically when connectivity is restored.
  • Caching and queues

    • Prefer local implementations when offline:
      • IDistributedCache → use a local provider (e.g., Memory) for the local host.
      • Distributed event bus → use local event bus (no broker) on the local host, and publish to the server only during sync phases.
  • Blazor hosting choice

    • Blazor Server requires a live SignalR connection to a server. For true offline usage, consider:
      • Running the local host on the same machine and connecting Blazor Server to it when offline, or
      • Moving to Blazor WebAssembly (PWA) for the UI so the UI itself loads offline and calls the local host API.
    • If you stay with Blazor Server, the “server” must be reachable locally during offline; that means your local host must be running and the client connects to it.
  • 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

  • ABP provides: modular layers, dynamic C# API clients, EF Core integration, background workers, caching abstraction.
  • You must implement: the local host, endpoint switching, offline auth strategy, and data synchronization logic and conflict handling.

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.

  1. Sources
  • ABP Framework GitHub (architecture, dynamic proxies, modules are visible in repo):
    • https://github.com/abpframework/abp

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.

  1. Solution Below is a definitive checklist and working setup to make domain/subdomain tenant resolution work with ABP 9.1.x for Blazor Interactive (WebAssembly-only) and a separated Auth Server (default OpenIddict). It addresses both backend resolution and the Blazor WASM client side, including environment-based subdomains like app-qa.example.com, app-pre.example.com, app.example.com with tenant1.app-qa.example.com, etc.

A) Backend (HttpApi.Host) – Resolve tenant from subdomain and allow wildcard OpenIddict/redirects

  1. Enable wildcard domain support for OpenIddict (required for subdomain tenants)
  • In YourHttpApiHostModule.PreConfigureServices:
// 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.
});
  1. Add Domain/Subdomain tenant resolver
  • In YourHttpApiHostModule.ConfigureServices:
// 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.
});
  1. If using a separated Auth Server (or validating tokens issued for wildcard domains)
  • Configure JWT bearer validation to accept wildcard issuers when you do token validation on gateway/API hosts that are not the issuer:
// 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/"
    };
});
  1. CORS and Redirect URIs for wildcard subdomains
  • Use ABP helpers that support wildcard subdomains for CORS and redirect URI validation:
context.Services.AddAbpStrictRedirectUriValidator();
context.Services.AddAbpClientConfigurationValidator();
context.Services.AddAbpWildcardSubdomainCorsPolicyService();
  • In DbMigrator appsettings.json (Identity/OpenIddict client registrations), set RootUrl with {0} placeholder so redirect URIs and post-logout URIs are generated per-tenant:
"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.

  1. Issuer URI when using OpenIddict If you customized IssuerUri earlier, ensure it does not conflict with wildcard expectations. Commonly you don’t set a fixed IssuerUri when using wildcard support; instead rely on AbpOpenIddictWildcardDomainOptions above.

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:

  1. In the Blazor WASM client’s Program.cs/Module.Initialize:
  • Read the current origin (builder.HostEnvironment.BaseAddress) and inject the tenant subdomain into the Authority and the RemoteServices BaseUrl by replacing {0}. The key is: never hardcode a single Authority; compute it based on current host.

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;
}
  1. Apply tenantized Authority and BaseUrl
  • When configuring authentication:
builder.Services.AddOidcAuthentication(options =>
{
    builder.Configuration.Bind("AuthServer", options.ProviderOptions);
    options.UserOptions.RoleClaim = "role";
    options.ProviderOptions.Authority = ConvertToTenantSubDomain(builder, "AuthServer:Authority");
});
  • When configuring remote services:
Configure<AbpRemoteServiceOptions>(options =>
{
    options.RemoteServices.Default = new RemoteServiceConfiguration(
        ConvertToTenantSubDomain(builder, "RemoteServices:Default:BaseUrl")
    );
});
  1. appsettings.json (Blazor client) Provide patterns with {0} in the Blazor client so the above replacement works:
{
  "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)

  • Add one resolver format per actual host pattern you expect to hit on the API/Auth side:
    • "{0}.app-qa.example.com"
    • "{0}.app-pre.example.com"
    • "{0}.app.example.com"
  • Make sure your DNS/ingress forwards those hosts to the API/Auth app.

F) Known limitations and notes for Blazor WASM

  • There is no server-side Blazor UI pipeline to run a tenant resolver in WASM; you must shape the Authority and BaseUrl on the client at startup as above. This is the recommended approach for WASM.
  • For the backend OpenIddict/redirect handling with subdomains, wildcard configuration is required (as in Section A).

G) Minimal verification steps

  1. Create a tenant named tenant1.
  2. Browse to https://tenant1.app-qa.example.com
  3. Observe the Blazor WASM client sets Authority/BaseUrl to https://tenant1.api.app-qa.example.com (or your pattern).
  4. Login succeeds; API requests include the correct tenant context, and ICurrentTenant is populated on the server.
  5. Browse in a separate tab to another tenant’s URL; it should remain isolated.
  1. Sources
  • https://abp.io/docs/latest/framework/architecture/multi-tenancy#domainsubdomain-tenant-resolver
  • https://github.com/abpframework/abp-samples/tree/master/DomainTenantResolver

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.

  1. Solution The behavior you see almost always comes from how ABP’s Http API pipeline applies:
  • Authorization
  • Anti-forgery
  • Conventional vs attribute routing
  • Frontend helper headers (X-Requested-With, Abp-Tenant, etc.)

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

    • Send a valid Bearer token: Authorization: Bearer <access_token>
  • Include the ABP Ajax header to avoid anti-forgery validation (GET is safe, but ABP expects the Ajax header in many UI scenarios)

    • X-Requested-With: XMLHttpRequest
  • If you enabled Anti-Forgery (default for MVC/Razor), either:

    • Send the RequestVerificationToken header (value from the hidden input/__RequestVerificationToken cookie) like Swagger does, or
    • Mark the endpoint to ignore antiforgery: [IgnoreAntiforgeryToken] [HttpGet("lookup/autocomplete")] public virtual Task<ListResultDto<VehicleAutocompleteDto>> AutocompleteAsync([FromQuery] string keyword) { ... }
  • Confirm your frontend is calling the exact same URL and method

    • GET http://localhost:44303/api/ksvr/vehicle-owners/lookup/autocomplete?keyword=29
    • If you have a reverse proxy/base path, ensure no extra prefix is added.
  • If your app is multi-tenant and the request needs tenant context

    • Send Abp-TenantId or Abp-Tenant (tenant name) header if required by your app.
    • Example: Abp-TenantId: <guid> or Abp-Tenant: <tenant-name>
  • Ensure route precedence is not overridden

    • You already placed [HttpGet("lookup/autocomplete")] before [HttpGet("{id:int}")], which is correct. If you have any global conventions modifying routes (like adding {__tenant}), confirm the final route matches what you call.
  • Test quickly with curl or Postman using the same headers Swagger shows

    • From Swagger “curl” box, copy all headers used there and try the same request outside Swagger. If it works, then add those headers to your web client.

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

  • The “by id” route might be hit from a tool that automatically includes Authorization (or your web call includes the token there), or your browser cached a token/cookie. The lookup route likely misses one of the headers above, causing ABP to return 404.
  1. Sources
  • https://docs.abp.io/en/abp/latest/API/Auto-API-Controllers

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.

  1. Solution
  • 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:

    • Domain tenant resolution supports full-domain patterns (whole-domain mapping), not only subdomains. You can add multiple domain formats or implement a custom tenant resolver to map arbitrary domains to tenants.
    • Per-tenant OIDC clients are first-class in OpenIddict module: you can persist multiple applications (clients) and select them per-tenant.
    • For server-rendered Blazor (OpenIdConnect handler), you can override the IssuerAddress on redirects; for WASM, set ProviderOptions.Authority at runtime based on current host.
    • Wildcard domain helpers are for subdomain patterns; since your domains are all different and not sharing a placeholder, do not use wildcard-domain feature. Seed exact redirect/cors origins for each tenant.
  • Step-by-step

A) Resolve tenant by arbitrary domain (no shared base):

  • Use DomainTenantResolver with multiple patterns when possible (hostnames that can be expressed as formats). For fully custom domains that don’t fit a single pattern, add a custom ITenantResolveContributor to map hostnames to tenants.
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());
        });
    }
}
  • Reference: ABP’s tenant resolvers and domain resolver (you can add your own contributor to handle full-domain mappings).

B) Store and use per-tenant OIDC client settings (ClientId, redirect/logout URLs):

  • Seed one OpenIddict application (client) per tenant with its exact redirect and logout URIs and allowed CORS origins. Do not rely on wildcard domain feature here, since your tenants don’t share a domain pattern.
  • You can seed OpenIddict applications in a data seeder that runs per tenant:
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.
    }
}
  • Important: Each tenant gets its own ClientId (e.g., contoso_blazor, fabrikam_blazor), and each uses its own exact redirect/logout URIs. No wildcard.

C) Make the Blazor Web App use the tenant’s client and authority dynamically

  • For Blazor Server/Web (OpenIdConnect handler), select authority and client based on the resolved tenant in redirect events, and select the ClientId per tenant. Store a mapping in a configuration or data store.
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; }
}
  • Register:
context.Services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, TenantOidcOptionsConfigurator>();
  • appsettings.json example:
{
  "TenantOidc": {
    "contoso": {
      "Authority": "https://api.contoso.com",
      "ClientId": "contoso_blazor"
    },
    "fabrikam": {
      "Authority": "https://api.fabrikam.net",
      "ClientId": "fabrikam_blazor"
    }
  }
}
  • For Blazor WASM: compute Authority and ClientId at runtime from the current host. Keep a dictionary in appsettings and select by the domain (or tenant name you resolved client-side).
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";
});
  • appsettings for WASM:
{
  "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

  • Since each tenant can have a different Authority/Issuer, configure JWT bearer validation to accept multiple issuers or implement a custom IssuerValidator. If all tenants share the same embedded OpenIddict server with a single issuer, keep a single Authority. If issuers differ by tenant, set ValidIssuers accordingly or implement a custom validator.
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

  • When tenant must be forced by domain, don’t expose tenant switching. With domain or custom resolver in place, the current tenant is already determined from the request host, and you can hide the switcher in the Account UI. ABP’s guidance: use domain-based resolver and do not rely on cookie/header switchers.

F) Summary checklist for your scenario (unique domains)

  • Implement a CustomDomainTenantResolveContributor to map arbitrary domains to tenants.
  • Seed one OpenIddict application per tenant with exact redirect/logout URIs and CORS origins. Use per-tenant ClientId.
  • In Blazor Server: configure OpenIdConnectOptions per request using ICurrentTenant to choose Authority and ClientId before redirecting.
  • In Blazor WASM: select Authority and ClientId at startup based on current host.
  • Configure JWT validation to support multiple issuers if necessary.
  • Do not enable wildcard domain feature for OpenIddict in this case, since domains are unrelated.
  • Ensure each tenant’s domain is registered in AllowedCorsOrigins and RedirectUris exactly.
  1. Sources
  • Multi-tenancy tenant resolution (domain/route and custom contributors):
    • https://docs.abp.io/en/abp/latest/Multi-Tenancy#tenant-resolution
    • https://abp.io/docs/latest/framework/architecture/multi-tenancy
  • OpenIddict module and deployment in ABP:
    • https://abp.io/docs/10.0//modules/openiddict
    • https://abp.io/docs/10.0//solution-templates/layered-web-application/deployment/openiddict-deployment
  • Server-side apps intercepting OIDC redirect to enforce proper Authority (pattern to adapt for per-tenant):
    • https://abp.io/docs/10.0//guides/ms-multi-tenant-domain-resolving

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.

  1. Solution
  • 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"
    }
  }
  • OpenIddict issuer and discovery handling (Blazor Server/Web/MVC if applicable): If you need to force login/logout to tenant-specific endpoints, adjust OIDC events to rewrite the IssuerAddress based on ICurrentTenant:
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).

  • Blazor Web App (WASM) – set Authority/BaseUrl dynamically per tenant from current domain: In the Blazor WASM project module (Program.cs or module class), compute URLs by replacing {0} with the tenant segment parsed from window.location.
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/"
          };
      });
  • Summary checklist:
    • Configure AbpTenantResolveOptions.AddDomainTenantResolver with your tenant domain format.
    • Enable AbpOpenIddictWildcardDomainOptions and add all wildcard formats used by clients (Blazor app, API, etc.).
    • In Blazor WASM, compute Authority/BaseUrl at runtime based on current host, replacing {0}.
    • If validating tokens with wildcard issuer, add wildcard issuer validator (when needed).
    • Do not use “*.domain.com” in redirect URIs; use ABP OpenIddict wildcard support as above.
  1. Sources
  • https://docs.abp.io/en/abp/latest/Multi-Tenancy#tenant-resolution
  • https://abp.io/docs/10.0//guides/ms-multi-tenant-domain-resolving
  • https://abp.io/docs/10.0//framework/architecture/multi-tenancy/

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.

  1. Solution
  • Root cause
    • In ABP v10 the default object mapper is Mapperly. Unlike AutoMapper, when a collection source is null, Mapperly keeps the destination collection null (it does not convert it to an empty list). Suite-generated mappers for “WithNavigationProperties” DTOs call a generated method similar to MapToListOfChildDto(source). If EF returns null for a child collection (e.g., Card.ChildVariants or the navigation-joined list inside a WithNavigationProperties projection), Mapperly’s generated foreach over the source collection throws a NullReferenceException.
  • Fix options
    1. Ensure collection properties are never null in your entities and DTOs:
      • Initialize child collections in the aggregate entity constructors.
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
       }
  1. Null-coalesce before mapping list properties in query projections to ensure the mapper receives an empty list instead of null:
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
                 };
  1. Add a small custom mapper to guard against null lists:
[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.
  1. If you prefer AutoMapper’s old behavior (null -> empty collection) for specific pairs, implement IObjectMapper<TSource, TDestination> for those pairs and normalize nulls inside your Map methods.
  • Why it happens now

    • This is an intentional behavior difference of Mapperly vs AutoMapper for collections. Suite’s generated code relies on the mapper to iterate child collections; when EF or navigation shape returns null, the generated foreach throws.
  • Quick checklist

    • Initialize all collection properties in:
      • Entities
      • WithNavigationProperties wrapper types
      • DTOs
    • In LINQ projections, coalesce collections: x.Children ?? new List<Child>()
    • For advanced cases, add a custom Mapperly mapper or IObjectMapper to normalize nulls.
  1. Sources
  • Validated links only:
    • https://github.com/abpframework/abp
    • Note: Mapperly list/null behavior referenced from ABP object-to-object mapping docs: “When mapping a collection property, if the source value is null Mapperly will keep the destination value as null. This is different from AutoMapper, which will map the destination field to an empty collection.” (ABP docs for v10 object-to-object mapping)

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.

Showing 21 to 30 of 739 entries
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.1.0-preview. Updated on December 17, 2025, 07:08
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.