Activities of "AI-Bot"

  • The error IDX10204: Unable to validate issuer. validationParameters.ValidIssuer is null or whitespace AND validationParameters.ValidIssuers is null or empty. means your API container cannot validate the issuer of the JWT token because the issuer in the token does not match the expected value, or the expected issuer is not configured.

  • In your docker-compose, you have:

    • OpenIddict__Validation__Issuer=https://localhost:44317/
    • OpenIddict__Validation__MetadataAddress=http://visaflowapp-authserver:8080/.well-known/openid-configuration
    • AuthServer__Authority=https://localhost:44317/

Common Issues and Solutions:

  1. Issuer Mismatch:

    • The issuer in the token must exactly match the value set in OpenIddict__Validation__Issuer and the issuer property in the discovery document (/.well-known/openid-configuration).
    • When running in Docker, localhost inside a container is not the same as localhost on your host. The API container will try to reach https://localhost:44317/ and fail, because that points to itself, not the AuthServer.
  2. Correct Issuer and Metadata Address:

    • Use the Docker service name for inter-container communication. For example:
      • OpenIddict__Validation__Issuer=http://visaflowapp-authserver:8080/
      • OpenIddict__Validation__MetadataAddress=http://visaflowapp-authserver:8080/.well-known/openid-configuration
    • Make sure the AuthServer's App__SelfUrl and AuthServer__Authority are also set to http://visaflowapp-authserver:8080 (for internal Docker network).
  3. HTTPS/HTTP Consistency:

    • If you use HTTP for internal Docker communication, set AuthServer__RequireHttpsMetadata=false and ensure all URLs (issuer, authority, metadata) use HTTP, not HTTPS, inside Docker.
    • Externally, you can expose HTTPS via Nginx or another reverse proxy.
  4. SetIssuer in Code:

    • In your AuthServer module, ensure you set the issuer explicitly:
PreConfigure<OpenIddictServerBuilder>(builder =>
     {
         builder.SetIssuer(new Uri(configuration["AuthServer:Authority"]!));
     });

Example Docker Compose Internal Configuration:

environment:
  - App__SelfUrl=http://visaflowapp-authserver:8080
  - AuthServer__Authority=http://visaflowapp-authserver:8080
  - AuthServer__RequireHttpsMetadata=false
  - OpenIddict__Validation__Issuer=http://visaflowapp-authserver:8080/
  - OpenIddict__Validation__MetadataAddress=http://visaflowapp-authserver:8080/.well-known/openid-configuration

Summary of Steps:

  1. Change all internal URLs to use the Docker service name and HTTP.
  2. Set RequireHttpsMetadata=false for internal Docker use.
  3. Ensure the issuer in the token, the discovery document, and your API config all match exactly.

References:

After making these changes, restart your containers. This should resolve the issuer validation error and allow authentication to work in Docker Compose.


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.

To allow your backend to accept requests from a locally running Blazor WebAssembly app while your services are deployed (e.g., on Kubernetes), you need to ensure proper CORS configuration in your microservice solution. In ABP 9.2.3, the CORS configuration approach remains similar, but you should also be aware of the X-Frame-Options header, which is set by default for security reasons.

Here’s how to configure your environment for local Blazor development with minimal impact:

  1. Update CORS Origins in appsettings.json or Helm Chart: Ensure your backend services (especially AuthServer and API gateways) have the following in their configuration:
"App": {
     "CorsOrigins": "https://localhost:44346,https://your-staging-domain.com"
   }
  • You can add multiple origins, separated by commas.
  • Make sure there are no trailing slashes.
  1. Apply CORS Middleware: In your service’s OnApplicationInitialization method, ensure:
app.UseCors();

This should be before app.UseAuthentication() and app.UseAuthorization().

  1. X-Frame-Options Issue: The error you see (Refused to display ... in a frame because it set 'X-Frame-Options' to 'SAMEORIGIN') is due to the default security headers. If you are embedding the AuthServer in an iframe (e.g., for login), you need to adjust the frame options. In your AuthServer, you can override the default header:
app.Use(async (context, next) =>
   {
       context.Response.Headers.Remove("X-Frame-Options");
       // Or set to allow from your localhost
       // context.Response.Headers["X-Frame-Options"] = "ALLOW-FROM https://localhost:44346";
       await next();
   });
  • Only do this for development; do not allow this in production for security reasons.
  1. Environment-Specific Configuration: Use environment variables or environment-specific appsettings (e.g., appsettings.Development.json) to include localhost only in development.

  2. Restart Services: After updating the configuration, restart your backend services to apply the changes.

  3. Browser Cache: Clear your browser cache or use an incognito window to avoid cached CORS or header responses.

This setup allows you to run your Blazor WebAssembly app locally and connect to your remote backend securely and conveniently.

Sources:


This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

To implement time-based permission grants in your ABP application, you need to extend the permission system to include optional StartTime and EndTime for each user-permission grant. Below is a step-by-step guide and code samples for both the backend (C#) and Angular frontend.


1. Backend: Extending Permission Grants

a. Extend the Permission Grant Entity

Create a new entity (e.g., UserTimeBasedPermission) to store user-permission grants with optional StartTime and EndTime.

public class UserTimeBasedPermission : Entity<Guid>
{
    public Guid UserId { get; set; }
    public string PermissionName { get; set; }
    public DateTime? StartTime { get; set; }
    public DateTime? EndTime { get; set; }
}

b. Create a Repository and Service

Define a repository and service to manage these grants.

public interface IUserTimeBasedPermissionRepository : IRepository<UserTimeBasedPermission, Guid>
{
    Task<UserTimeBasedPermission> FindAsync(Guid userId, string permissionName);
}

public class UserTimeBasedPermissionRepository : EfCoreRepository<YourDbContext, UserTimeBasedPermission, Guid>, IUserTimeBasedPermissionRepository
{
    public UserTimeBasedPermissionRepository(IDbContextProvider<YourDbContext> dbContextProvider)
        : base(dbContextProvider)
    {
    }

    public async Task<UserTimeBasedPermission> FindAsync(Guid userId, string permissionName)
    {
        return await (await GetDbSetAsync())
            .FirstOrDefaultAsync(x => x.UserId == userId && x.PermissionName == permissionName);
    }
}

c. Implement a Custom Permission Value Provider

Create a custom PermissionValueProvider to check the time window.

public class TimeBasedPermissionValueProvider : PermissionValueProvider
{
    public const string ProviderName = "TimeBased";
    private readonly IUserTimeBasedPermissionRepository _repository;
    private readonly ICurrentUser _currentUser;

    public TimeBasedPermissionValueProvider(
        IUserTimeBasedPermissionRepository repository,
        ICurrentUser currentUser)
        : base(ProviderName)
    {
        _repository = repository;
        _currentUser = currentUser;
    }

    public override async Task<PermissionValueProviderGrantInfo> CheckAsync(PermissionValueCheckContext context)
    {
        if (!_currentUser.Id.HasValue)
            return PermissionValueProviderGrantInfo.Prohibited();

        var grant = await _repository.FindAsync(_currentUser.Id.Value, context.Permission.Name);
        if (grant == null)
            return PermissionValueProviderGrantInfo.Undefined();

        var now = DateTime.UtcNow;
        if (grant.StartTime.HasValue && now < grant.StartTime.Value)
            return PermissionValueProviderGrantInfo.Prohibited();
        if (grant.EndTime.HasValue && now > grant.EndTime.Value)
            return PermissionValueProviderGrantInfo.Prohibited();

        return PermissionValueProviderGrantInfo.Granted();
    }
}

Register your provider in the module:

Configure<AbpPermissionOptions>(options =>
{
    options.ValueProviders.Add<TimeBasedPermissionValueProvider>();
});

d. Grant/Update Permissions

Create an application service to manage time-based grants:

public class TimeBasedPermissionAppService : ApplicationService
{
    private readonly IUserTimeBasedPermissionRepository _repository;

    public TimeBasedPermissionAppService(IUserTimeBasedPermissionRepository repository)
    {
        _repository = repository;
    }

    public async Task GrantAsync(Guid userId, string permissionName, DateTime? startTime, DateTime? endTime)
    {
        var grant = await _repository.FindAsync(userId, permissionName);
        if (grant == null)
        {
            grant = new UserTimeBasedPermission
            {
                Id = Guid.NewGuid(),
                UserId = userId,
                PermissionName = permissionName,
                StartTime = startTime,
                EndTime = endTime
            };
            await _repository.InsertAsync(grant);
        }
        else
        {
            grant.StartTime = startTime;
            grant.EndTime = endTime;
            await _repository.UpdateAsync(grant);
        }
    }
}

2. Angular Frontend: Managing Time-Based Permissions

a. Create a Service to Call the API

@Injectable({ providedIn: 'root' })
export class TimeBasedPermissionService {
  constructor(private http: HttpClient) {}

  grant(userId: string, permissionName: string, startTime?: string, endTime?: string): Observable<void> {
    return this.http.post<void>('/api/app/time-based-permission/grant', {
      userId,
      permissionName,
      startTime,
      endTime
    });
  }
}

b. Create a UI Component

@Component({
  selector: 'app-time-based-permission',
  template: `
    &lt;form (ngSubmit)=&quot;grant()&quot;&gt;
      &lt;input [(ngModel)]=&quot;userId&quot; name=&quot;userId&quot; placeholder=&quot;User ID&quot; required /&gt;
      &lt;input [(ngModel)]=&quot;permissionName&quot; name=&quot;permissionName&quot; placeholder=&quot;Permission Name&quot; required /&gt;
      &lt;input [(ngModel)]=&quot;startTime&quot; name=&quot;startTime&quot; type=&quot;datetime-local&quot; placeholder=&quot;Start Time&quot; /&gt;
      &lt;input [(ngModel)]=&quot;endTime&quot; name=&quot;endTime&quot; type=&quot;datetime-local&quot; placeholder=&quot;End Time&quot; /&gt;
      &lt;button type=&quot;submit&quot;&gt;Grant&lt;/button&gt;
    &lt;/form&gt;
  `
})
export class TimeBasedPermissionComponent {
  userId = '';
  permissionName = '';
  startTime?: string;
  endTime?: string;

  constructor(private service: TimeBasedPermissionService) {}

  grant() {
    this.service.grant(this.userId, this.permissionName, this.startTime, this.endTime).subscribe(() => {
      alert('Permission granted!');
    });
  }
}

3. Usage

  • Use the Angular component to grant permissions with optional time windows.
  • The backend will enforce the time window on every permission check.

4. Notes

  • This approach does not modify the built-in ABP permission management UI, but provides a custom API and UI for time-based grants.
  • You can further extend this to integrate with the ABP permission management module if needed.

Sources:


This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

  1. To change the built-in ABP API route prefix (such as /api/abp/application-configuration and /api/abp/application-localization), ABP does not provide a global, officially supported way to rename all built-in endpoints’ prefixes (like "abp") out-of-the-box. These endpoints are hardcoded in the ABP modules and are used by the Angular UI and ABP’s internal mechanisms. Overriding them would require forking the relevant ABP modules and updating all usages, which is not recommended as it will break compatibility with future ABP updates and may cause issues with the Angular UI, which expects these endpoints.

  2. To change the generated CSS bundle name (e.g., abp-bundle.css), you can customize the bundle name using ABP’s bundling system. In your module’s ConfigureServices method, configure the AbpBundlingOptions and set your desired bundle name:

Configure<AbpBundlingOptions>(options =>
{
    options.StyleBundles.Configure(
        StandardBundles.Styles.Global,
        bundleConfiguration =>
        {
            bundleConfiguration.FileName = "company-bundle.css"; // Set your custom bundle name here
        }
    );
});

Reference:

  1. For static files and headers, you can override or customize most static assets and headers by using ABP’s bundling and theming system, as shown above. However, for built-in API endpoints, there is no officially supported, update-safe way to globally replace "abp" in all routes and headers. Attempting to do so would require deep customization and is not recommended for long-term maintainability.

Summary:

  • You can change bundle/static file names using AbpBundlingOptions.
  • You cannot globally and safely change the "abp" prefix in all built-in API endpoints without forking ABP modules, which is not recommended.
  • For white-labeling and branding, focus on customizing UI, static files, and visible assets, but accept that some internal endpoints will retain the "abp" prefix for compatibility.

Sources:


This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

  • This is a known issue with abp-extensible-table in Chromium-based browsers after upgrading to Angular 20 and ABP 9.3.1. The table header misalignment on initial render is caused by how ngx-datatable (used internally by abp-extensible-table) calculates column widths, which can be affected by rendering timing differences in Chromium browsers.

  • Your workaround (setting canAutoResize = false on ngx-datatable-column) is a valid temporary solution, as it prevents automatic width recalculation that can cause the misalignment.

  • There is an open issue tracking a similar problem with extensible forms and typeahead, which may be related to rendering/patching timing in Angular 16+ and Chromium:

    • https://github.com/abpframework/abp/issues/22375
    • https://abp.io/support/questions/8965
  • For a more robust workaround until an official fix is released, you can:

    1. Set canAutoResize = false on all columns in your abp-extensible-table.
    2. Alternatively, after the table is rendered, programmatically trigger a table resize or redraw (e.g., by calling the ngx-datatable's recalculate() method or dispatching a window resize event).
  • The ABP team is aware of similar rendering issues and is working on improvements for extensible components in upcoming releases. You can follow the progress on the GitHub issue above.

Sources:

  • https://github.com/abpframework/abp/issues/22375
  • https://abp.io/support/questions/8965

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.

Answer

Your issue is related to concurrency exceptions in OpenIddict token management, which can cause unexpected logouts or failed refreshes, especially when multiple tabs or clients attempt to refresh tokens simultaneously. This is a known challenge in ABP/OpenIddict-based systems, particularly before the introduction of improved concurrency handling in later ABP versions.

Solution Steps

1. Understand the Root Cause

  • The error DbUpdateConcurrencyException occurs when two or more requests try to update or revoke the same refresh token at the same time. OpenIddict uses optimistic concurrency for token updates, so only one request will succeed; others will fail, potentially invalidating the session for the user.
  • This is especially likely when multiple browser tabs or clients share the same refresh token and attempt to refresh at nearly the same time.

2. Mitigation Strategies for ABP v7.3.3

Since AbpEfCoreNavigationHelper and related improvements are not available in v7.3.3, you need to implement workarounds:

A. Prevent Parallel Refresh Token Requests

  • Ensure that your Angular client does not send multiple /connect/token requests in parallel for the same user/session.
  • Implement a mutex/lock mechanism in your Angular service to serialize refresh token requests. Only allow one refresh at a time, and queue or deduplicate others.

Example (pseudo-code for Angular service):

private refreshInProgress = false;
private refreshPromise: Promise<any> | null = null;

refreshToken(): Promise<any> {
  if (this.refreshInProgress) {
    return this.refreshPromise!;
  }
  this.refreshInProgress = true;
  this.refreshPromise = this.authService.refreshToken().finally(() => {
    this.refreshInProgress = false;
    this.refreshPromise = null;
  });
  return this.refreshPromise;
}
  • Use this method in your HTTP interceptor or wherever you handle token refreshes.

B. Handle Concurrency Exceptions Gracefully

  • If a concurrency exception occurs, catch it and prompt the user to re-authenticate, rather than logging them out everywhere.
  • Optionally, implement a retry mechanism with a short delay, as sometimes the token state will resolve after the first failed attempt.

C. Reduce Token Lifetime or Use Reference Tokens

  • Consider increasing the access token lifetime if possible, to reduce refresh frequency.
  • Alternatively, use reference tokens (if supported by your OpenIddict setup) to minimize the impact of concurrency.

3. Upgrade Recommendation

  • The most robust solution is to upgrade to a newer ABP version (>=8.0) where concurrency handling for OpenIddict tokens has been improved. If possible, plan for an upgrade to benefit from these fixes.

4. Additional Resources


By serializing refresh token requests on the client and handling concurrency exceptions gracefully, you can significantly reduce or eliminate these intermittent logout issues in ABP v7.3.3. For a long-term fix, consider upgrading to a newer ABP version with improved OpenIddict concurrency support.

Sources:


This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

To achieve platform-wide (host-level) translations for all tenants in ABP Framework, especially for built-in ABP resources that do not have a Serbian translation, you need to ensure that your dynamic localization implementation always falls back to the host's language texts when a tenant-specific translation is missing.

Your current MyDynamicResourceLocalizer implementation always loads language texts from the host (TenantId = null) by using:

using (currentTenant.Change(null))
{
    var texts = scope.ServiceProvider
        .GetRequiredService<ILanguageTextRepository>()
        .GetList(resource.ResourceName, cultureName);

    foreach (var text in texts)
    {
        cacheItem.Dictionary[text.Name] = text.Value;
    }
}

This is correct for host-level translations, but it means that tenant-specific overrides will never be used. However, if your tenants are not seeing all translations, it is likely because:

  1. The Serbian translation for ABP's built-in resources does not exist in the ABP source or in your database.
  2. The LanguageText table for tenants is empty or incomplete for Serbian, so only host-level texts are available.
  3. The Language Management UI only shows texts that exist in the database, not those from embedded JSON files or ABP's default resources.

Key Points & Solution:

  • ABP's built-in resources (e.g., AbpUi, AbpAccount, etc.) do not have a Serbian translation by default. If you want to provide a Serbian translation, you must add the translations yourself, either via the Language Management UI or by providing your own JSON files.
  • Your MyDynamicResourceLocalizer is already set up to always use host-level translations, which is the correct approach for platform-wide language texts.
  • If you want to allow tenants to override translations, you should first check for a tenant-specific translation, and if not found, fall back to the host translation.

Recommended Implementation:

To support both host-wide and tenant-specific translations, update your MyDynamicResourceLocalizer like this:

[Dependency(ReplaceServices = true)]
public class MyDynamicResourceLocalizer : DynamicResourceLocalizer
{
    private readonly ICurrentTenant _currentTenant;

    public MyDynamicResourceLocalizer(
        IServiceScopeFactory serviceScopeFactory,
        IDistributedCache<LanguageTextCacheItem> cache,
        ICurrentTenant currentTenant)
        : base(serviceScopeFactory, cache)
    {
        _currentTenant = currentTenant;
    }

    protected override LanguageTextCacheItem CreateCacheItem(LocalizationResourceBase resource, string cultureName)
    {
        var cacheItem = new LanguageTextCacheItem();

        using (var scope = ServiceScopeFactory.CreateScope())
        {
            var languageTextRepository = scope.ServiceProvider.GetRequiredService<ILanguageTextRepository>();

            // 1. Load tenant-specific translations (if any)
            var tenantTexts = languageTextRepository.GetList(resource.ResourceName, cultureName);
            foreach (var text in tenantTexts)
            {
                cacheItem.Dictionary[text.Name] = text.Value;
            }

            // 2. Load host translations for missing keys
            using (_currentTenant.Change(null))
            {
                var hostTexts = languageTextRepository.GetList(resource.ResourceName, cultureName);
                foreach (var text in hostTexts)
                {
                    if (!cacheItem.Dictionary.ContainsKey(text.Name))
                    {
                        cacheItem.Dictionary[text.Name] = text.Value;
                    }
                }
            }
        }

        return cacheItem;
    }
}
  • This ensures that tenant-specific translations override host translations, but host translations are used as a fallback.
  • If you want only host-level translations (no tenant overrides), your original implementation is correct.

For ABP built-in resources in Serbian:

  • You must provide the Serbian translations yourself, either by importing them into the Language Management module or by adding custom JSON files for each resource (e.g., /Localization/AbpUi/sr-Latn.json).

Summary:

  • Your approach is correct for host-wide translations.
  • If you want fallback logic (tenant override, then host), use the code above.
  • For ABP built-in resources, you must provide the Serbian translations yourself.

Sources:

  • https://abp.io/support/questions/6636
  • https://abp.io/support/questions/8778
  • https://abp.io/docs/9.0/framework/fundamentals/localization#extending-existing-resource
  • https://abp.io/community/articles/g1a6lgdl

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.

To add SEO meta tags and ld+json structured data to CMS pages created via the ABP CMS Kit (MVC Core Razor):

  1. Extend the CMS Page Rendering:

    • Override the Razor Page or View that renders your CMS Kit dynamic pages (typically under /Pages/Cms/Pages/Detail.cshtml or similar).
    • In this view, access the page's metadata (title, description, etc.) from the CMS Kit's page entity.
  2. Add Meta Tags:

    • In the <head> section of your layout or the specific page view, inject meta tags dynamically using the page's properties.
@model Volo.CmsKit.Public.Pages.PageDto
   @{
       Layout = "_Layout";
   }
   <head>
       <title>@Model.Title</title>
       <meta name="description" content="@Model.Description" />
       <meta property="og:title" content="@Model.Title" />
       <meta property="og:description" content="@Model.Description" />
       <!-- Add more meta tags as needed -->
   </head>
  1. Add ld+json Structured Data:
    • Insert a <script type="application/ld+json"> block in the page, using Razor to serialize your data.
<script type="application/ld+json">
   @{
       var jsonLd = new {
           "@context" = "https://schema.org",
           "@type" = "Article",
           "headline" = Model.Title,
           "description" = Model.Description,
           "datePublished" = Model.CreationTime.ToString("yyyy-MM-dd"),
           "author" = new { "@type" = "Person", "name" = Model.CreatorName }
       };
       var json = System.Text.Json.JsonSerializer.Serialize(jsonLd);
   }
   @Html.Raw(json)
   </script>
  1. Custom Fields (Optional):

    • If you need custom SEO fields (like custom meta description or keywords), extend the CMS Kit Page entity and admin UI to support these fields, then render them as above.
  2. Reference:

    • For more on customizing CMS Kit pages, see the official documentation and demo source code.

Sources:


This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

To resolve the error "Could not find the BLOB Storage provider with the type (Volo.Abp.BlobStoring.Database.DatabaseBlobProvider...)", you must ensure that:

  1. The Database BLOB provider NuGet package is installed in your project:
dotnet add package Volo.Abp.BlobStoring.Database
  1. The module dependency is declared in your module class (e.g., in your *.HttpApi.Host or main module):
[DependsOn(typeof(AbpBlobStoringDatabaseModule))]
   public class YourModule : AbpModule
   {
       // ...
   }
  1. The provider is configured in ConfigureServices:
Configure<AbpBlobStoringOptions>(options =>
   {
       options.Containers.ConfigureDefault(container =>
       {
           container.UseDatabase();
       });
       // Your other container configurations...
   });
  1. If you are using the File Management or Account module (which uses AccountProfilePictureContainer), ensure the default provider is set or that you configure the specific container:
options.Containers.Configure<AccountProfilePictureContainer>(container =>
   {
       container.UseDatabase();
   });
  1. If you have multiple providers (e.g., Minio and Database), you must configure each container explicitly to use the correct provider. The default provider is only used for containers that are not explicitly configured.

  2. Restart your application after making these changes.

This will register the Database BLOB provider and resolve the exception.

Sources:


This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

Answer

The slow performance of ABP permission cache (PermissionGrants) during the first login is a known behavior, especially when there are a large number of permissions or roles. The method PermissionStore.SetCacheItemsAsync is responsible for loading and caching all granted permissions for the first time, which can be time-consuming if the dataset is large. Subsequent logins are fast because the data is served from the cache.

Key points and recommendations:

  • The first load after a cache clear (or on a cold start) will query all permission grants from the database and set them in the cache, which can take several seconds if you have thousands of permissions or roles.
  • After the initial load, subsequent requests are fast due to caching.
  • To improve the first-load performance:
    • Reduce the number of permission grants if possible.
    • Optimize your database (indexes, hardware, etc.).
    • Consider pre-warming the cache after deployment or application restart.
    • If using distributed cache (e.g., Redis), ensure the cache server is performant and network latency is low.

For more details and discussion, see the official support thread:

  • https://abp.io/support/questions/9767/The-abp-permission-cache-PermissionGrants-loads-very-slowly-during-the-first-login-but-very-quickly-during-the-second-login-When-the-permission-cache-is-manually-cleared-it-slows-down-again

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 51 to 60 of 385 entries
Learn More, Pay Less
33% OFF
All Trainings!
Get Your Deal
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.0.0-preview. Updated on September 12, 2025, 10:20