[Collection(MyAppTestConsts.CollectionDefinitionName)]
public class EfCore_ProductAppService_Tests
: ProductAppService_Tests<MyAppEntityFrameworkCoreTestModule>
{
}
public static class MyAppTestConsts
{
public const string CollectionDefinitionName = "MyApp collection";
}
[CollectionDefinition(MyAppTestConsts.CollectionDefinitionName)]
public class MyAppCollection : ICollectionFixture<MyAppEntityFrameworkCoreFixture>
{
}
public class MyAppEntityFrameworkCoreFixture : IDisposable
{
public SqliteConnection Connection { get; }
public MyAppEntityFrameworkCoreFixture()
{
Connection = new SqliteConnection("Data Source=:memory:");
Connection.Open();
}
public void Dispose() => Connection.Dispose();
}
[DependsOn(typeof(MyAppApplicationTestModule), typeof(MyAppEntityFrameworkCoreModule), typeof(AbpEntityFrameworkCoreSqliteModule))]
public class MyAppEntityFrameworkCoreTestModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var conn = context.Services.GetRequiredService<MyAppEntityFrameworkCoreFixture>().Connection;
Configure<AbpDbContextOptions>(options =>
{
options.Configure(cfg => cfg.DbContextOptions.UseSqlite(conn));
});
}
}
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
Checklist to fix:
// Mobile (React Native) client
await CreateClientAsync(
name: "MyApp_Mobile",
scopes: new[] { "openid", "profile", "email", "offline_access", "MyService" },
grantTypes: new[] { "authorization_code" },
secret: null, // PKCE, public client
requireClientSecret: false,
redirectUri: "com.myapp.mobile://callback",
postLogoutRedirectUri: "com.myapp.mobile://signout-callback",
corsOrigins: new[] { "https://your-api-host", "https://your-auth-host" }
);
"App": {
"SelfUrl": "https://api.staging.myproject.io",
"CorsOrigins": "https://*.myproject.io,http://localhost:19006,exp://127.0.0.1:19000",
"RedirectAllowedUrls": "com.myapp.mobile://callback,com.myapp.mobile://signout-callback"
},
"AuthServer": {
"Authority": "https://auth.staging.myproject.io",
"RequireHttpsMetadata": "true"
}
Why 400 appears:
Quick minimal set you can copy:
await CreateClientAsync(
name: "MyApp_Mobile",
scopes: new[] { "openid", "profile", "email", "offline_access", "MyService" },
grantTypes: new[] { "authorization_code" },
requireClientSecret: false,
redirectUri: "com.myapp.mobile://callback",
postLogoutRedirectUri: "com.myapp.mobile://signout-callback",
corsOrigins: new[] { "https://auth.staging.myproject.io", "https://api.staging.myproject.io" }
);
"App": {
"CorsOrigins": "https://*.myproject.io,http://localhost:19006,exp://127.0.0.1:19000",
"RedirectAllowedUrls": "com.myapp.mobile://callback,com.myapp.mobile://signout-callback"
}
export const oAuthConfig = {
issuer: 'https://auth.staging.myproject.io',
clientId: 'MyApp_Mobile',
redirectUri: 'com.myapp.mobile://callback',
scopes: ['openid', 'profile', 'email', 'offline_access', 'MyService'],
};
If you apply the above, the React Native login should complete and the token exchange should return 200 with tokens instead of 400.
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
Goal: Customize the creation flow for Tenants (SaaS module) and Users (Identity module) in ABP (e.g., validate additional inputs, create related domain data, call external systems, or change defaults).
Change the Tenant creation flow (SaaS)
using Volo.Abp;
using Volo.Abp.Application.Dtos;
using Volo.Abp.DependencyInjection;
using Volo.Saas.Tenants;
using Volo.Abp.Application.Services;
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(ITenantAppService))]
public class MyTenantAppService : TenantAppService, ITenantAppService
{
public MyTenantAppService(ITenantRepository tenantRepository, ITenantManager tenantManager)
: base(tenantRepository, tenantManager)
{
}
public override async Task<TenantDto> CreateAsync(TenantCreateDto input)
{
// 1) Custom validation
if (string.IsNullOrWhiteSpace(input.Name))
throw new BusinessException("TenantNameRequired");
// 2) Pre-processing (e.g., reserve subdomain, call payment API)
// await _myBillingClient.CreateCustomerAsync(input.Name, ...);
var result = await base.CreateAsync(input);
// 3) Post-processing (e.g., seed additional data for the new tenant)
await SeedForTenantAsync(result.Id);
return result;
}
public override async Task<TenantDto> UpdateAsync(Guid id, TenantUpdateDto input)
{
// Custom logic before update
var updated = await base.UpdateAsync(id, input);
// Custom logic after update
return updated;
}
private async Task SeedForTenantAsync(Guid tenantId)
{
using (CurrentTenant.Change(tenantId))
{
// Create default domain aggregates, settings, features, etc.
// await _myAppService.InitializeDefaultsAsync();
}
}
}
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus;
using Volo.Saas.Tenants;
public class TenantCreatedHandler :
ILocalEventHandler<TenantCreatedEto>, ITransientDependency
{
public async Task HandleEventAsync(TenantCreatedEto eventData)
{
using (CurrentTenant.Change(eventData.Id))
{
// Seed default data, features, etc.
}
}
}
Change database provisioning behavior
Change the User creation flow (Identity)
using Volo.Abp.DependencyInjection;
using Volo.Abp.Identity;
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IIdentityUserAppService))]
public class MyIdentityUserAppService : IdentityUserAppService, IIdentityUserAppService
{
public MyIdentityUserAppService(IdentityUserManager userManager, IIdentityUserRepository userRepository, IIdentityRoleRepository roleRepository)
: base(userManager, userRepository, roleRepository)
{
}
public override async Task<IdentityUserDto> CreateAsync(IdentityUserCreateDto input)
{
// Custom validation (e.g., enforce password policy, email domain checks)
// if (!input.Email.EndsWith("@mycorp.com")) throw new BusinessException("InvalidEmailDomain");
var user = await base.CreateAsync(input);
// Post actions: assign roles/permissions, create related domain data
// await UserManager.AddToRoleAsync(await UserManager.GetByIdAsync(user.Id), "MyDefaultRole");
return user;
}
}
using Volo.Abp.EventBus;
using Volo.Abp.Identity;
public class UserCreatedHandler :
ILocalEventHandler<IdentityUserCreatedEto>, ITransientDependency
{
public async Task HandleEventAsync(IdentityUserCreatedEto eventData)
{
using (CurrentTenant.Change(eventData.TenantId))
{
// E.g., create a profile aggregate, send welcome mail, assign features based on tenant plan
}
}
}
- Alternatively, derive from IdentityUserManager and override CreateAsync to enforce invariants at the domain layer, then register your manager with ReplaceServices.
Validation, permissions, and DTO mapping
Multi-tenancy considerations
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
Goal: Use Keycloak (as the OpenID Connect provider) instead of ABP’s built-in Auth Server/OpenIddict in a multi-tenant ABP application.
High-level approach:
Backend (HttpApi.Host or Gateway)
// In YourProjectNameHttpApiHostModule.ConfigureServices
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
context.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
// Keycloak realm OIDC metadata
options.Authority = configuration["AuthServer:Authority"]; // e.g. https://keycloak.example.com/realms/myrealm
options.RequireHttpsMetadata = true;
options.TokenValidationParameters.ValidateAudience = false; // or set ValidAudience(s) if you validate it
// Optional: Map Keycloak claim names to ABP’s defaults if needed
options.TokenValidationParameters.NameClaimType = ClaimTypes.Name;
options.TokenValidationParameters.RoleClaimType = ClaimTypes.Role;
});
// Multi-tenancy tenant resolution (domain-based example)
Configure<AbpTenantResolveOptions>(opts =>
{
// e.g. {0}.myapp.com → tenant from subdomain
opts.AddDomainTenantResolver("{0}.myapp.com");
// Optionally also allow header/query resolvers if you prefer those
// opts.AddHeaderTenantResolver();
// opts.AddQueryStringTenantResolver();
});
// CORS if needed for your SPA domains
Configure<AbpCorsOptions>(options =>
{
options.AddPolicy(DefaultCorsPolicyName, builder =>
{
builder
.WithOrigins(
"https://{0}.myapp.com" // replace with exact SPA origins per tenant if not using wildcard CORS service
)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
}
// In your module’s OnApplicationInitialization (or Program.cs in minimal hosting)
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
app.UseCorrelationId();
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseMultiTenancy();
app.UseAuthorization();
app.UseConfiguredEndpoints();
}
oAuthConfig: {
issuer: 'https://keycloak.example.com/realms/myrealm',
redirectUri: 'https://{0}.myapp.com', // domain-based tenant
clientId: 'my-angular-client',
responseType: 'code',
scope: 'openid profile email offline_access',
requireHttps: true
}
- Use domain-based tenant resolution on the UI by setting baseUrl/Authority with {0} placeholder and configure AbpTenantResolveOptions on the backend to match. This prevents tenant switching and ensures the __tenant value aligns with the subdomain.
- When navigating to login, optionally pass __tenant if you rely on header/query resolvers:
import { AuthService, SessionStateService } from '@abp/ng.core';
// ...
const tenantId = this.sessionState.getTenant()?.id;
this.authService.navigateToLogin(tenantId ? { __tenant: `${tenantId}` } : undefined);
Blazor WebAssembly:
Keycloak configuration
Multi-tenancy specifics
Configure<AbpTenantResolveOptions>(options =>
{
options.AddDomainTenantResolver("{0}.myapp.com");
});
Ensure headers with underscores (__tenant) are forwarded if you use header/query resolvers behind proxies like NGINX (set underscores_in_headers on). With pure domain resolver, this is not needed.
Place UseAuthentication() before UseMultiTenancy() in the middleware order so tenant can be resolved from token/headers correctly where applicable.
Common pitfalls
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 Centralized Authentication (one Auth Server) and OIDC sign-in, not password-based token requests:
Automatic user provisioning on first external login (recommended):
Don’t reuse the same confidential client credentials across different sites arbitrarily:
When only email is available (no password):
Practical steps
services.AddAuthentication()
.AddCookie()
.AddOpenIdConnect("oidc", "AuthServer", options =>
{
options.Authority = configuration["AuthServer:Authority"];
options.RequireHttpsMetadata = true;
options.ClientId = configuration["AuthServer:ClientId"];
options.ClientSecret = configuration["AuthServer:ClientSecret"];
options.ResponseType = "code";
options.UsePkce = true;
options.SaveTokens = true;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.Scope.Add("offline_access");
options.Scope.Add("MySolution2Api"); // your API scope(s)
// Important for ABP external login handling:
options.ClaimActions.MapJsonKey(System.Security.Claims.ClaimTypes.NameIdentifier, "sub");
options.GetClaimsFromUserInfoEndpoint = true; // or false if you rely on id_token only
});
- Ensure cookies are configured and the app uses the OIDC challenge for sign-in.
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.
LeptonX MVC layouts (including the top menu layout) are Razor files in the LeptonX theme package. To customize the top menu, override the corresponding .cshtml files in your Web project by copying them with the exact same folder structure. ABP will automatically prefer your local files over the theme’s embedded ones.
Steps:
Get the LeptonX theme source (optional but helpful to see the original files):
Override the top menu layout in your MVC app:
Switch your application to use the top menu layout:
Notes/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.
What changed in v10.0.1
Why it “works in Application layer but not in Blazor”
Two supported ways to resolve
Option A — Keep using AutoMapper everywhere (quickest to keep existing profiles)
using Volo.Abp;
using Volo.Abp.AutoMapper;
using Volo.Abp.Modularity;
[DependsOn(
typeof(AbpAutoMapperModule)
)]
public class MyProjectBlazorModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAutoMapperObjectMapper<MyProjectBlazorModule>();
Configure<AbpAutoMapperOptions>(options =>
{
// Add your AutoMapper profiles that contain CreateMap<,>() definitions
options.AddMaps<MyProjectApplicationModule>(validate: true);
options.AddMaps<MyProjectBlazorModule>(validate: true);
});
}
}
Option B — Migrate to Mapperly (recommended for ABP v10+)
using Riok.Mapperly.Abstractions;
using Volo.Abp.Mapperly;
[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
public partial class MyEntityToDtoMapper : MapperBase<MyEntity, MyEntityDto>
{
public override partial MyEntityDto Map(MyEntity source);
public override partial void Map(MyEntity source, MyEntityDto destination);
}
using Volo.Abp;
using Volo.Abp.Mapperly;
using Volo.Abp.Modularity;
[DependsOn(typeof(AbpMapperlyModule))]
public class MyProjectBlazorModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddMapperlyObjectMapper<MyProjectBlazorModule>();
}
}
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.
Why it happens
What to change
Option A: Use WithDetailsAsync in your AppService
public virtual async Task<ProductItemDto> GetAsync(Guid id)
{
var queryable = await _productItemRepository.WithDetailsAsync(
x => x.ProductItemCultures,
x => x.ProductItemAttributeListValues
);
var entity = await AsyncExecuter.FirstOrDefaultAsync(queryable.Where(x => x.Id == id));
if (entity == null)
{
throw new EntityNotFoundException(typeof(ProductItem), id);
}
return ObjectMapper.Map<ProductItem, ProductItemDto>(entity);
}
public virtual async Task<ProductItemDto> GetAsync(Guid id)
{
var entity = await _productItemRepository.GetAsync(id, includeDetails: true);
return ObjectMapper.Map<ProductItem, ProductItemDto>(entity);
}
public class ProductItemRepository
: EfCoreRepository<MyDbContext, ProductItem, Guid>, IProductItemRepository
{
public ProductItemRepository(IDbContextProvider<MyDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
public override async Task<IQueryable<ProductItem>> WithDetailsAsync()
{
return (await GetQueryableAsync())
.Include(x => x.ProductItemCultures)
.Include(x => x.ProductItemAttributeListValues);
}
}
With this, any GetAsync(id, includeDetails: true) will include those navigations by default.
Notes for multi-tenancy and filters
using (DataFilter.Disable<ISoftDelete>())
{
// load with details here
}
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.
Short answer: Yes, you can ship a pure-OSS HttpApi.Client package (and underlying contracts) by removing all ABP Commercial (Pro) dependencies from Domain.Shared, Application.Contracts and HttpApi.Client, while still using Commercial packages in your server-side layers. However:
Practical steps:
Licensing implications:
Technical limitations to watch:
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
using Volo.Abp.Security.Claims;
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.Configure<AbpClaimsPrincipalFactoryOptions>(options =>
{
options.IsDynamicClaimsEnabled = true; // enable dynamic claims
});
}
using Volo.Abp.Security.Claims;
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
context.Services.Configure<AbpClaimsPrincipalFactoryOptions>(options =>
{
options.IsDynamicClaimsEnabled = true;
// If you use separate Auth Server, prepend the absolute authority to the default refresh path
options.RemoteRefreshUrl = configuration["AuthServer:Authority"]?.TrimEnd('/') + options.RemoteRefreshUrl;
});
}
Notes:
If you use Abp’s AddAbpOpenIdConnect/AddAbpJwtBearer, the RemoteRefreshUrl is set automatically. The explicit assignment above is useful when you configure auth handlers manually or need to override.
Ensure the UI uses the Auth Server authority consistently (AuthServer:Authority must point to your Identity/Auth host URL).
Implement and register your dynamic claims contributor (on the Auth Server; in tiered setups, this must run where the principal is created/refreshed):
using System.Security.Claims;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Security.Claims;
public class MyDynamicClaimsContributor :
IDynamicClaimsPrincipalContributor, ITransientDependency
{
public async Task ContributeAsync(DynamicClaimsPrincipalContributionContext context)
{
// Example: add/update a claim dynamically, e.g. a custom tenant role/flag
// Use context.Principal to read current claims and context.TenantId, context.UserId, etc.
var identity = context.Principal.Identity as ClaimsIdentity;
if (identity == null) return;
// remove existing value if present
identity.RemoveClaim(identity.FindFirst("my-dynamic-flag"));
// add new dynamic claim
identity.AddClaim(new Claim("my-dynamic-flag", "true"));
await Task.CompletedTask;
}
}
Then register it (Auth Server):
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpDynamicClaimsPrincipalOptions>(options =>
{
options.Contributors.Add<MyDynamicClaimsContributor>();
});
}
RemoteServices configuration: which remote service is used?
Authentication middleware/handlers:
context.Services.AddAuthentication()
.AddAbpOpenIdConnect("oidc", options =>
{
options.Authority = configuration["AuthServer:Authority"];
// other OIDC options (ClientId, Secret, ResponseType, scopes ...)
// RemoteRefreshUrl is set by AddAbpOpenIdConnect internally.
});
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.