Activities of "AI-Bot"

  1. Solution Based on your code and the fact that it works locally but fails after deployment when using text templates, the root causes are typically:
  • Email sender not configured for production (ABP uses NullEmailSender in Debug).
  • Templates are being read from the physical file system, which doesn’t exist in the deployed environment (container/IIS/bin path differences).
  • Using ITemplateRenderer without registering/embedding templates in the ABP Virtual File System (VFS).

Fix it with the following steps:

A) Ensure email sending is enabled in Production

  • Remove/disable NullEmailSender replacement in production builds. In your host module, only replace IEmailSender with NullEmailSender in DEBUG:
public override void ConfigureServices(ServiceConfigurationContext context)
{
#if DEBUG
    context.Services.Replace(ServiceDescriptor.Singleton<IEmailSender, NullEmailSender>());
#endif
}
  • Provide SMTP/MailKit settings in production (e.g., appsettings.Production.json, environment variables, or ABP Setting Management DB). Minimum settings:
"Settings": {
  "Abp.Mailing.Smtp.Host": "smtp.yourprovider.com",
  "Abp.Mailing.Smtp.Port": "587",
  "Abp.Mailing.Smtp.UserName": "user",
  "Abp.Mailing.Smtp.Password": "<ENCRYPTED_PASSWORD>",
  "Abp.Mailing.Smtp.EnableSsl": "true",
  "Abp.Mailing.Smtp.UseDefaultCredentials": "false",
  "Abp.Mailing.DefaultFromAddress": "no-reply@yourdomain.com",
  "Abp.Mailing.DefaultFromDisplayName": "Your App"
}

Note: The SMTP password must be stored encrypted if you put it in settings. Use IStringEncryptionService to encrypt before saving to the DB or write a small snippet at startup to call SettingManager.SetGlobalAsync for the password (ABP encrypts on set, decrypts on get).

B) Stop loading templates from the physical file system; embed and use ABP VFS + ITemplateRenderer Right now, LoadTemplate reads files via File.ReadAllTextAsync from AppContext.BaseDirectory/Emailing/Templates. This commonly breaks in containers, single-file publish, or when paths differ.

Use ABP’s text templating + virtual file system:

  1. Add your template files to the Domain (or another project) and mark them as Embedded Resource, e.g.:
  • Emailing/Templates/UserCreation.tpl
  • Emailing/Templates/ChangePassword.tpl
  1. Register the embedded files in the module:
[DependsOn(typeof(AbpEmailingModule))]
public class YourProjectDomainModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        Configure<AbpVirtualFileSystemOptions>(options =>
        {
            options.FileSets.AddEmbedded<YourProjectDomainModule>();
        });
    }
}
  1. Define template keys and bind them to the embedded paths:
public static class MyEmailTemplates
{
    public const string UserCreation = "MyEmailTemplates.UserCreation";
    public const string ChangePassword = "MyEmailTemplates.ChangePassword";
}

public class EmailTemplateDefinitionProvider : TemplateDefinitionProvider
{
    public override void Define(ITemplateDefinitionContext context)
    {
        context.Add(
            new TemplateDefinition(MyEmailTemplates.UserCreation)
                .WithVirtualFilePath("/Emailing/Templates/UserCreation.tpl", isInlineLocalized: true)
        );

        context.Add(
            new TemplateDefinition(MyEmailTemplates.ChangePassword)
                .WithVirtualFilePath("/Emailing/Templates/ChangePassword.tpl", isInlineLocalized: true)
        );
    }
}
  1. Render with ITemplateRenderer using the keys (no manual file IO):
private async Task<string> RenderEmailBodyAsync(string templateName, object model)
{
    // Map friendly names to your template keys
    var templateKey = templateName.Equals("UserCreation.tpl", StringComparison.OrdinalIgnoreCase)
        ? MyEmailTemplates.UserCreation
        : MyEmailTemplates.ChangePassword;

    return await _templateRenderer.RenderAsync(templateKey, model);
}
  1. Update your EmailNotification method to call RenderEmailBodyAsync instead of LoadTemplate (and remove all file system logic). Your current per-template Replace code is not needed; ITemplateRenderer will inject the model values if your tpl uses ABP’s Scriban syntax, e.g.:
  • In tpl: Hello {{ model.Name }}!
  • In code: new { Name = input.Name, Url = "...", ... }

C) If you want to keep raw HTML templates and simple Replace (without Scriban)

  • Still embed the files and load via VFS provider:
private readonly IFileProvider _fileProvider;
public SendGridEmailNotificationAppServiceBase(..., IVirtualFileProvider vfs, ...)
{
    _fileProvider = vfs;
}

private async Task<string> LoadTemplateFromVfsAsync(string virtualPath)
{
    var file = _fileProvider.GetFileInfo(virtualPath); // e.g. "/Emailing/Templates/UserCreation.tpl"
    if (!file.Exists) throw new FileNotFoundException($"Template not found: {virtualPath}");
    using var stream = file.CreateReadStream();
    using var reader = new StreamReader(stream);
    return await reader.ReadToEndAsync();
}
  • Then keep your Replace code but call LoadTemplateFromVfsAsync("/Emailing/Templates/UserCreation.tpl"). This avoids AppContext.BaseDirectory issues in production.

D) Ensure environment-specific URLs are configured

  • You use _configuration["App:SelfUrl"] for BaseSiteUrl and also refer to angular URL for navigation. Provide correct values in production appsettings (or environment variables) so links in emails are valid.

E) Validate multi-tenancy handling and encryption

  • Your tenantName encryption is fine; just ensure you URL-encode the encrypted string (you already do with EscapeDataString). Confirm that decryption works on the login page.

F) Use MailKit in production (recommended) If you see warnings about SmtpClient, add AbpMailKit and configure MailKit sender:

  • Add package Volo.Abp.MailKit and depend on AbpMailKitModule.
  • Configure settings the same way; ABP routes to MailKit automatically when module is present.

G) Quick checklist for deployed environment

  • SMTP/MailKit settings are present and correct (host/port/SSL/username/password).
  • Password stored encrypted or set via SettingManager on startup.
  • Remove debug-only NullEmailSender in production.
  • Email templates are embedded and accessed via ABP VFS or via ITemplateRenderer with template definitions.
  • No direct File.ReadAllTextAsync to content paths.
  • App:SelfUrl/AngularUrl configured for production.

Code summary (minimal changes to your class):

  • Replace your LoadTemplate method with ITemplateRenderer calls as shown in B.4.
  • Register VFS + TemplateDefinitionProvider in your module.
  • Keep IEmailSender usage as-is.
  1. Sources
  • https://abp.io/docs/latest/deployment/configuring-production
  • https://abp.io/community/articles/jkeb8zzh
  • https://abp.io/support/questions/226

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
  • About the 403 error and licensing
    • Volo.Payment is a Commercial (PRO) module. Installing it with abp add-module requires your ABP account to be a member of an organization with an active Commercial license (Team or higher). Otherwise, the CLI returns 403 Forbidden with the “PRO modules require a commercial license” message.
    • Verify your current CLI session and organization:
      1. Run: abp login-info
      2. Check that:
        • Logged in: True
        • Email matches app-access@lexarius.com
        • OrganizationName and LicenseType show your company and an active license (Team/Business/Enterprise). If Organization is empty or LicenseType is not active, the CLI won’t allow adding Volo.Payment.
      3. If the login is stale or points to a wrong org, refresh: abp logout abp login app-access@lexarius.com abp login-info
      4. If you indeed have a Commercial license but your user is not linked to the org, ask your organization owner/admin to invite this email to the licensed organization from the ABP account portal. If you don’t have a Commercial license, you need to obtain one (Team or higher) to use Volo.Payment.
      5. If you still see licensing errors after confirming membership and license, clear local session and retry:
        • abp logout
        • Delete the ABP CLI auth cache folder on the machine (if any), then abp login again.
  • Implementing cart, checkout, payment, and access granting in ABP
    • Recommended building blocks:
      • Domain entities: Course, Order, OrderItem, PaymentRecord (or Payment), Enrollment (UserCourse)
      • Application services: CartAppService, OrderAppService, PaymentAppService, EnrollmentAppService
      • Integration with Payment: Prefer ABP’s Payment Module (PRO) for Stripe/PayPal/etc. If you don’t/can’t use it, you can integrate Stripe SDK directly in your app service following ABP’s layered architecture and unit of work.
    • High-level flow (with ABP Payment Module)
      1. Cart:
        • Persist cart items per user (e.g., a Cart aggregate or transient client-side cart sent at checkout).
        • Validate availability/prices via application service.
      2. Create Order (domain-driven):
        • In an application service method, map cart items to an Order aggregate and OrderItems; calculate totals; save Order with status PendingPayment.
      3. Create Payment Request:
        • Use the Payment Module to create a payment request associated with the Order ID and total amount; select the gateway (Stripe).
        • Redirect the user to Stripe checkout (hosted) or collect payment method and confirm on the server, depending on gateway flow.
      4. Handle payment success callback/webhook:
        • Implement the gateway callback/webhook endpoint. In the handler:
          • Verify signature/intent status.
          • Update PaymentRecord to Succeeded and Order to Paid in a single unit of work.
          • Publish a domain event like OrderPaidEvent.
      5. Grant access on success:
        • Handle OrderPaidEvent (or subscribe to the PaymentSucceeded event if using the Payment Module) to create Enrollment rows per purchased Course: repository.InsertAsync(new Enrollment(userId, courseId));
        • Optionally, assign permissions/roles or feature flags if access is permission-based, but a simple Enrollment join table is typical.
      6. Authorization:
        • In course access queries/controllers, filter by current user’s enrollments to authorize content access.
    • Example skeleton
      • Domain event after payment success:
public class OrderPaidEvent : EntityEventData<Order>
      {
          public OrderPaidEvent(Order entity) : base(entity) {}
      }
- Event handler to grant access:
public class OrderPaidEventHandler : ILocalEventHandler<OrderPaidEvent>, ITransientDependency
      {
          private readonly IRepository<Enrollment, Guid> _enrollmentRepo;

          public OrderPaidEventHandler(IRepository<Enrollment, Guid> enrollmentRepo)
          {
              _enrollmentRepo = enrollmentRepo;
          }

          public async Task HandleEventAsync(OrderPaidEvent eventData)
          {
              var order = eventData.Entity;
              foreach (var item in order.Items)
              {
                  await _enrollmentRepo.InsertAsync(new Enrollment(order.UserId, item.CourseId), autoSave: true);
              }
          }
      }
- Guarding course content:
public class CourseAppService : ApplicationService
      {
          private readonly IRepository<Enrollment, Guid> _enrollmentRepo;
          private readonly IRepository<Course, Guid> _courseRepo;

          public async Task<CourseDto> GetAsync(Guid courseId)
          {
              var isEnrolled = await _enrollmentRepo.AnyAsync(e => e.UserId == CurrentUser.GetId() && e.CourseId == courseId);
              if (!isEnrolled) throw new AbpAuthorizationException("Not enrolled");

              var course = await _courseRepo.GetAsync(courseId);
              return ObjectMapper.Map<Course, CourseDto>(course);
          }
      }
  • If you cannot use the PRO Payment Module:
    • Keep the same domain flow (Order, PaymentRecord, events).
    • Integrate Stripe Checkout/Payment Intents in your application service and controller. Persist PaymentRecord with external transaction IDs. On webhook, update Order and raise the same OrderPaidEvent to centralize the access granting logic.
  • Multi-tenancy and consistency:
    • Use ABP UnitOfWork in application services and event handlers.
    • Use background jobs only if post-payment steps are long-running; otherwise handle synchronously inside the same UoW triggered by the webhook/event.
  1. Sources
  • ABP Module System basics:
    • https://abp.io/docs/latest/framework/architecture/modularity/basics
  • ABP Payment Module (Commercial/PRO) overview:
    • https://abp.io/docs/latest/modules/payment
  • ABP new CLI reference page mentioned in your log:
    • https://abp.io/new-cli
  • ABP Commercial and PRO modules/licensing references (context on PRO modules and access):
    • 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
  • Reuse one Document Management module in multiple hosts
    • Package the module and consume it from both Host 1 and Host 2:
      1. Split the module into the standard ABP layers: MyDoc.Domain.Shared, MyDoc.Domain, MyDoc.Application.Contracts, MyDoc.Application, MyDoc.HttpApi, MyDoc.HttpApi.Client, MyDoc.EntityFrameworkCore (and optional UI layer).
      2. Publish each layer as a NuGet package (internal feed or private NuGet) and reference them from both hosts.
      3. In each host, add [DependsOn] to include the module in the host’s module classes (Web/HttpApi.Host, Application, Domain, EF Core).
[DependsOn(
           typeof(MyDocApplicationModule),
           typeof(MyDocHttpApiModule),
           typeof(MyDocEntityFrameworkCoreModule)
       )]
       public class HostAppHttpApiHostModule : AbpModule
       {
       }
4) Add the module’s EF Core Configure call into each host’s migrations DbContext (or migrations project) so the host’s DbMigrator migrates the module tables:
protected override void OnModelCreating(ModelBuilder builder)
       {
           base.OnModelCreating(builder);
           builder.ConfigureMyDoc(); // your module's extension method
       }
5) Run each host’s DbMigrator to create/update databases.
  • This is the recommended way to reuse functionality across multiple ABP applications (module as a package rather than project reference), keeping hosts loosely coupled.

  • Generating Angular proxies without ABP Suite entities

    • ABP Angular proxies are generated from your backend’s OpenAPI/Swagger, not from Suite entities. As long as your module exposes HTTP APIs (e.g., via Conventional Controllers in MyDoc.HttpApi), you can generate Angular proxies:
      1. Ensure the host that exposes the module APIs runs and serves Swagger (e.g., https://localhost:44300/swagger/v1/swagger.json).
      2. In your Angular app, configure the proxy generator to point to the service’s Swagger:
        • Using ABP CLI:
          • Run: abp generate-proxy -t angular -u https://localhost:44300 -m YourAngularProjectRoot
        • Or configure angular.json/ng-proxy to point to the remote; ABP Angular package will pull the OpenAPI and generate TypeScript clients.
      3. If you package and distribute MyDoc.HttpApi.Client as a NuGet and also publish an NPM package for the Angular UI of your module, you can reuse even more easily; however, it is not mandatory. The key is your module must publish HTTP APIs; proxies are generated from those APIs’ Swagger definition, regardless of ABP Suite usage.
  • Database segregation approaches when reusing the same module in two hosts

    • Option A: One database per host
      • Each host has its own connection string(s). Both include the module and run their own migrations. Tables for the module will exist in each host’s DB (fully isolated).
    • Option B: Shared database across hosts (same connection string)
      • Both hosts include the module, but only one host (or a dedicated migrator) should be responsible for running migrations to avoid conflicts. Both apps work on the same module tables.
    • Option C: Multiple DbContexts or multiple DBMS per host (advanced)
      • If you want the module to use a dedicated database different from the host’s main DB, define a specific DbContext in the module and mark it with [ConnectionStringName("MyDoc")]. In the host appsettings.json, provide "ConnectionStrings:MyDoc". In the module’s EF Core module, configure provider per DbContext:
Configure<AbpDbContextOptions>(options =>
      {
          options.Configure<MyDocDbContext>(opt => { opt.UseSqlServer(); /* or UseMySQL etc. */ });
      });
- This allows Host 1 and Host 2 to point the module to different databases or even different DBMS per host while sharing the same binary module. See also the note below for multiple providers per solution.
  • Deployment separation

    • If you choose separate DBs per host, deploy DbMigrator (or EF tooling) per host environment to apply that host’s migrations.
    • Package versioning: keep the module versioned and update both hosts independently via NuGet upgrades.
    • Angular: Each host’s Angular app can either:
      • Generate proxies directly from the host’s own backend that exposes the module’s APIs, or
      • Consume proxies from a static generated client library published to a private NPM feed. Both are valid; ABP recommends static client proxies for loose coupling.
  • Notes and tips

    • You do not need ABP Suite entities to generate Angular proxies. Proxies are generated from Swagger of your HTTP APIs.
    • When using multiple DbContexts and/or multiple DB providers in one solution, configure AbpDbContextOptions per DbContext (not globally) to prevent one provider overriding another. Use [ConnectionStringName] on each DbContext and provide separate connection strings in appsettings per host.
    • For migrations placement: for Angular UI solutions, you typically keep migrations in the HttpApi.Host (or a dedicated DbMigrations project used by DbMigrator). If your module is reused in different host types, each host should include the module’s ConfigureXxx(builder) in its migrations context and run its own migrations.
  1. Sources
  • Validated: ABP Module System basics
    • https://abp.io/docs/latest/framework/architecture/modularity/basics
  • Validated: Customizing/reusing existing modules
    • https://abp.io/docs/latest/framework/architecture/modularity/extending/customizing-application-modules-guide
  • Validated: Multiple DbContexts and provider configuration discussion (per-DbContext configuration tip referenced)
    • 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. Solution Based on the new Global Assets feature, global.js is not a physical file you build with abp bundle anymore; it is generated dynamically by the host (server) project at runtime and served from a virtual file provider. To make https://localhost/global.js available in a Blazor Web App (ABP v10), ensure all of the following:
  • Use the correct bundling modules on the host (server) project

    • Add a dependency to one of the WebAssembly theme bundling modules (choose the one you use):
      • AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeBundlingModule
      • AbpAspNetCoreComponentsWebAssemblyLeptonXThemeBundlingModule
      • AbpAspNetCoreComponentsWebAssemblyBasicThemeBundlingModule
      • AbpAspNetCoreComponentsWebAssemblyLeptonThemeBundlingModule
    • Also keep AbpAspNetCoreMvcUiBundlingModule on the host project; it publishes the global assets as virtual files.

    Example:

[DependsOn(
      typeof(AbpAutofacModule),
      typeof(AbpAspNetCoreMvcUiBundlingModule),
      typeof(AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeBundlingModule)
      // + any commercial WebAssembly bundling modules you use, e.g.:
      // typeof(AbpAuditLoggingBlazorWebAssemblyBundlingModule)
  )]
  public class MyProjectNameBlazorModule : AbpModule
  {
      public override void ConfigureServices(ServiceConfigurationContext context)
      {
          // Add services to the container
          context.Services.AddRazorComponents()
              .AddInteractiveWebAssemblyComponents();
      }

      public override void OnApplicationInitialization(ApplicationInitializationContext context)
      {
          var app = context.GetApplicationBuilder();
          var env = context.GetEnvironment();

          if (env.IsDevelopment())
          {
              app.UseWebAssemblyDebugging();
          }
          else
          {
              app.UseHsts();
          }

          app.UseHttpsRedirection();

          // IMPORTANT: maps static/virtual assets including global.js & global.css
          app.MapAbpStaticAssets();

          app.UseRouting();
          app.UseAntiforgery();

          app.UseConfiguredEndpoints(builder =>
          {
              builder.MapRazorComponents<App>()
                  .AddInteractiveWebAssemblyRenderMode()
                  .AddAdditionalAssemblies(WebAppAdditionalAssembliesHelper.GetAssemblies<MyProjectNameBlazorClientModule>());
          });
      }
  }
  • Put your bundle contributor in the client project and contribute to the correct bundle name
    • In the Blazor.Client project, register your contributor to BlazorWebAssemblyStandardBundles.Scripts.Global:
public class MonacoBundleScriptContributor : BundleContributor
  {
      public override void ConfigureBundle(BundleConfigurationContext context)
      {
          context.Files.AddIfNotContains("_content/BlazorMonaco/jsInterop.js");
          context.Files.AddIfNotContains("_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.js");
          context.Files.AddIfNotContains("_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.js");
      }
  }

  // In the same Blazor.Client project (e.g., in your module)
  private void ConfigureBundles()
  {
      Configure<AbpBundlingOptions>(options =>
      {
          var globalScripts = options.ScriptBundles.Get(BlazorWebAssemblyStandardBundles.Scripts.Global);
          globalScripts.AddContributors(typeof(MonacoBundleScriptContributor));
      });
  }

Notes:

  • Use AddIfNotContains and relative paths starting with _content/ for library static files.

  • Ensure the Blazor.Client project references the BlazorMonaco package so those _content files exist at runtime.

  • Reference the client project from the host project

    • The server (host) project must have a ProjectReference to the Blazor.Client project so it can access its static/virtual contents and build the dynamic global bundle.
  • Ensure MapAbpStaticAssets is called in the host pipeline

    • Without app.MapAbpStaticAssets(), the dynamic virtual files (/global.js and /global.css) won’t be served.
  • Verify at runtime

    • Run the host project and browse:
      • https://localhost:PORT/global.js
      • https://localhost:PORT/global.css
    • If you changed the file names via AbpBundlingOptions.GlobalAssets, use that path accordingly (defaults are global.js and global.css).
  • Common pitfalls to check

    • Missing theme WebAssembly bundling module on the host: without it, the bundling pipeline for WebAssembly is not wired, so global.js won’t be produced.
    • Contributor registered on the wrong bundle or wrong project: contributors should be registered against BlazorWebAssemblyStandardBundles.Scripts.Global (not MVC bundles) and typically from the Blazor.Client project.
    • Not calling MapAbpStaticAssets in the host app.
    • Paths incorrect: library static files must be referenced with _content/LibraryName/... and the package must be referenced so the files are available.
  1. Sources
  • https://abp.io/docs/latest/framework/architecture/modularity/basics
  • https://docs.abp.io/en/abp/latest/UI/AspNetCore/Bundling-Minification
  • https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2024-11-25-Global-Assets/POST.md

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 verified, end-to-end way to implement a self-service “upgrade/downgrade edition” flow with ABP Commercial’s SaaS + Payment modules (Stripe), including proration and without bypassing the Payment module. It reuses the built-in subscription pipeline and webhooks so tenant edition/renewal dates are updated by ABP.

Key points

  • Use the Payment module’s Subscription flow, not direct Stripe API calls.
  • Use the IWeb pages provided by Volo.Payment.Web/Volo.Payment.Stripe.Web for gateway selection and pre-payment.
  • ABP handles proration/dates via the gateway (Stripe “price”) and its webhook handlers that update the tenant’s edition association.
  • For now, creating a subscription via ISubscriptionAppService requires host-side execution to read Edition metadata. For tenant self-service, create a PaymentRequest with PaymentType=Subscription and attach Edition/Plan identifiers via ExtraProperties; Stripe webhook + Payment module will complete the edition change.
  • Downgrades should also be done as subscription changes (switch the Plan/GatewayPlan), letting Stripe/Payment handle timing/proration.

Step-by-step

A) Install and wire modules

  • Add Payment and Stripe packages and depends-on attributes:
    • Domain, Domain.Shared: Volo.Payment, Volo.Payment.Stripe
    • Application, Application.Contracts, HttpApi, HttpApi.Client: Volo.Payment, Volo.Payment.Admin
    • EFCore: Volo.Payment.EntityFrameworkCore and call builder.ConfigurePayment() in your DbContext; add migration/update database.
    • Web/MVC (AuthServer or Public Web where you show self-service pages): Volo.Payment.Web and Volo.Payment.Stripe.Web so you can redirect to built-in pages:
      • /Payment/GatewaySelection
      • /Payment/Stripe/PrePayment

B) Configure Payment and SaaS

  • In HttpApi.Host (where your payment APIs run):
    • Enable payment integration for SaaS: Configure<AbpSaasPaymentOptions>(opts => opts.IsPaymentSupported = true);
    • appsettings.json:
"Payment": {
      "Stripe": {
        "PublishableKey": "pk_test_xxx",
        "SecretKey": "sk_test_xxx",
        "WebhookSecret": "whsec_xxx",
        "Currency": "USD",        // set as needed
        "Locale": "auto",
        "PaymentMethodTypes": []  // leave empty; module adds “card” automatically
      }
    }
  • In your Web or AuthServer module, where you host UI pages:
    • PreConfigure PaymentWebOptions so the built-in pages know where to come back:
public override void PreConfigureServices(ServiceConfigurationContext context)
    {
        var configuration = context.Services.GetConfiguration();
        Configure<PaymentWebOptions>(options =>
        {
            options.RootUrl = configuration["App:SelfUrl"];
            options.CallbackUrl = configuration["App:SelfUrl"] + "/PaymentSucceed";
        });
    }

C) Define SaaS Editions, Plans and Stripe mapping

  • Create/Configure:
    • Edition(s)
    • Plan(s) under editions
    • Gateway Plan(s) for Stripe: ExternalId must be the Stripe price_id (not product_id). This enables proration/recurring behavior consistent with Stripe price configuration.

D) Self-service “change plan” UI flow (tenant-facing)

  • Recommended approach for tenant self-service is creating a PaymentRequest with PaymentType=Subscription and redirecting to GatewaySelection. You can carry EditionId/TenantId in ExtraProperties, so webhook processing can finalize the edition change.

Example MVC page model (tenant UI):

public class ChangePlanModel : PageModel
  {
      private readonly IPaymentRequestAppService _paymentRequestAppService;
      private readonly ICurrentTenant _currentTenant;
      public ChangePlanModel(IPaymentRequestAppService paymentRequestAppService, ICurrentTenant currentTenant)
      {
          _paymentRequestAppService = paymentRequestAppService;
          _currentTenant = currentTenant;
      }

      public async Task<IActionResult> OnPostAsync(Guid editionId, Guid planId)
      {
          // Carry needed info for webhook/finalization
          var pr = await _paymentRequestAppService.CreateAsync(
              new PaymentRequestCreateDto
              {
                  Products =
                  {
                      new PaymentRequestProductCreateDto
                      {
                          PaymentType = PaymentType.Subscription,
                          Name = "PlanChange",
                          Code = $"{_currentTenant.Id}_{planId}",
                          Count = 1,
                          PlanId = planId
                      }
                  }
              }.WithExtra(new ExtraPropertyDictionary
              {
                  { "EditionId", editionId },
                  { "TenantId", _currentTenant.Id }
              })
          );

          return LocalRedirectPreserveMethod("/Payment/GatewaySelection?paymentRequestId=" + pr.Id);
      }
  }

Helper extension (optional):

public static class PaymentRequestDtoExtensions
  {
      public static PaymentRequestCreateDto WithExtra(this PaymentRequestCreateDto dto, ExtraPropertyDictionary extras)
      {
          dto.ExtraProperties = extras;
          return dto;
      }
  }

Notes

  • This uses PaymentRequestAppService, so tenant users don’t need host-level permissions to call ISubscriptionAppService. The Payment module + Stripe will handle starting/changing the subscription. The built-in webhook will read the PaymentRequest and update the tenant’s edition and dates.
  • The change (upgrade/downgrade) is driven by the plan/price you pass (PlanId mapped to a Stripe price). Stripe proration behavior applies automatically if you configured proration in Stripe price/subscription settings.

E) Redirects and built-in UI

  • After creating the payment request, redirect to:
    • /Payment/GatewaySelection?paymentRequestId=...
      • If only one gateway is active, it will auto-forward to:
    • /Payment/Stripe/PrePayment?paymentRequestId=...
      • This calls the Payment API (POST /api/payment/stripe/start) and then redirects the user to Stripe Checkout.
  • On success, Stripe returns to PaymentWebOptions.CallbackUrl (e.g., /PaymentSucceed). Meanwhile, Stripe sends webhook events that ABP handles to finalize the subscription and update the SaasTenant edition/renewal data.

F) Webhooks are mandatory

  • Ensure the Stripe webhook endpoint is configured in Stripe to point to your HttpApi.Host base URL with the ABP Stripe webhook path. Use the WebhookSecret in appsettings. On local dev, use Stripe CLI to forward events. Without webhooks, the edition won’t update automatically.

G) Downgrade and upgrade are the same flow

  • Present the tenant with available plans (including current plan).
  • When they pick a new plan (higher or lower), create a new PaymentRequest (PaymentType=Subscription, PlanId = new plan) and redirect as above.
  • Stripe will calculate proration; ABP webhook handler will update the tenant’s edition end date and plan info accordingly.

H) If you prefer ISubscriptionAppService

  • You can continue to use ISubscriptionAppService.CreateSubscriptionAsync(editionId, tenantId) but today it must be executed in host context to read Edition data; tenant-side calls can get 403. The tenant-flow via PaymentRequest shown above avoids that constraint and stays within supported APIs.

I) Angular UI

  • Admin UI management is provided by @volo/abp.ng.payment. For tenant self-service, you can use a simple Angular component that calls your backend endpoint to create a PaymentRequest and then redirects the browser to /Payment/GatewaySelection?paymentRequestId=... hosted on your MVC/AuthServer app. This reuses the built-in pages for selection and pre-payment.

Common pitfalls and fixes

  • GatewayPlan.ExternalId must be Stripe price_id, not product_id.
  • In appsettings, set "PaymentMethodTypes": [] to avoid duplications; the module adds “card.”
  • Remember to add Volo.Payment.Web and Volo.Payment.Stripe.Web to the project that renders the redirect destination, and configure PaymentWebOptions.RootUrl/CallbackUrl.
  • Ensure EF migration for Payment tables is applied.
  • Verify the webhook secret and that the webhook actually reaches your API Host.
  1. Sources
  • https://abp.io/docs/latest/modules/payment
  • https://abp.io/docs/latest/modules/payment#subscription
  • https://abp.io/support/questions/7847

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 Your AuthServer is rejecting the authorization request because the redirect_uri sent by the Angular app (http://10.136.36.151:4200) is not registered as an allowed redirect/callback URL for the client NovaLab_App.

Do the following:

  • Add the Angular URL(s) to RedirectAllowedUrls (ABP/OpenIddict)
    • In your AuthServer (or HttpApi.Host for non-tiered Angular apps), open appsettings.json and set:
"App": {
    "SelfUrl": "https://10.136.36.151:44380",
    "RedirectAllowedUrls": "http://localhost:4200,http://10.136.36.151:4200,https://10.136.36.151:4200"
  }

Notes:

  • Include every URL the SPA can use: local dev, IP:port, domain names, and HTTPS variants (if applicable).

  • If you terminate TLS elsewhere but serve Angular over HTTPS, include the https:// variant.

  • Restart the AuthServer after changing appsettings.

  • Ensure the OpenIddict client (NovaLab_App) has the exact redirect URIs

    • If you seed clients in code (e.g., OpenIddictDataSeedContributor), ensure NovaLab_App includes:
      • RedirectUris: http://10.136.36.151:4200
      • PostLogoutRedirectUris: http://10.136.36.151:4200
      • Also include http://localhost:4200 for local dev.
    • Re-run migrator/seed or update via OpenIddict management UI if you have it.
    • Redirect URIs must match exactly (scheme, host, port, path).
  • Align Angular environment configuration

    • In angular/src/environments/environment.prod.ts (and environment.ts), verify:
      • oAuthConfig.issuer points to your AuthServer public URL (https://10.136.36.151:44380).
      • oAuthConfig.redirectUri equals the SPA origin used by the browser (e.g., http://10.136.36.151:4200).
      • oAuthConfig.postLogoutRedirectUri matches the same origin.
    • The SPA’s redirectUri must be one of the allowed RedirectUris you registered.
  • Don’t mix schemes unintentionally

    • Your log shows AuthServer at https://10.136.36.151:44380 while Angular is http://10.136.36.151:4200. This is fine as long as http://10.136.36.151:4200 is explicitly allowed.
    • If you switch Angular to HTTPS later, add the https://:4200 variant too.
  • Reverse proxy/containers tip

    • If running behind a proxy, ensure the AuthServer sees the correct external scheme/host (use ForwardedHeaders middleware). This doesn’t change redirect_uri validation but prevents other OIDC issues.

After these changes, restart services. The login should proceed without “invalid redirect_uri”.

  1. Sources
  • OpenIddict deployment note (register RedirectAllowedUrls for Angular/Blazor):
    • https://abp.io/docs/latest/solution-templates/layered-web-application/deployment/openiddict-deployment

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 ABP UI renders global CSS/JS through the bundling system and the Virtual File System. In production containers, the “Could not find file '~/global-styles.css'” error typically happens because the file is not included in any bundle or the static/virtual files are not mapped/packaged into the image.

Apply all steps below:

  • Ensure you add your CSS into the Basic Theme global bundle (so ABP can serve it virtually)
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
  using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Bundling;

  public class ProdigyEnablerWebModule : AbpModule
  {
      public override void ConfigureServices(ServiceConfigurationContext context)
      {
          Configure<AbpBundlingOptions>(options =>
          {
              options.StyleBundles.Configure(BasicThemeBundles.Styles.Global, bundle =>
              {
                  // Path must be relative to web root (wwwroot)
                  bundle.AddFiles("/styles/global-styles.css");
              });
          });
      }
  }

Notes:

  • Place the file at WebProject/wwwroot/styles/global-styles.css.
  • Don’t reference “~/global-styles.css” directly in your layout; use the bundle tag helper:
<abp-style-bundle name="@BasicThemeBundles.Styles.Global" />
  • Map ABP static assets in the ASP.NET Core pipeline (for virtual/static files to be reachable)
public override void OnApplicationInitialization(ApplicationInitializationContext context)
  {
      var app = context.GetApplicationBuilder();
      var env = context.GetEnvironment();

      if (!env.IsDevelopment())
      {
          app.UseHsts();
      }

      app.UseHttpsRedirection();
      app.UseStaticFiles();
      app.MapAbpStaticAssets(); // important for ABP’s virtual files
      app.UseRouting();
      app.UseAuthentication();
      app.UseAuthorization();
      app.UseConfiguredEndpoints();
  }
  • Ensure client libraries and bundles are present when you build/publish:

    • If your UI depends on npm/yarn packages (e.g., theme packages), run install-libs during CI:
      • Add a step in your Dockerfile or CI to execute from the Web project folder:
        • dotnet tool restore
        • dotnet tool update -g Volo.Abp.Cli (optional)
        • abp install-libs
    • Then publish:
      • dotnet publish -c Release -o /app/publish
    • Copy the published output only in your final image (multi-stage build).
  • Dockerfile (reference pattern)

    • Multi-stage example to guarantee libs and wwwroot are included:
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
    WORKDIR /src

    # Copy solution and restore
    COPY ./ ./
    RUN dotnet restore

    # Install ABP libs for the Web project (adjust path to your Web csproj folder)
    WORKDIR /src/YourCompany.YourProj.Web
    RUN dotnet tool restore
    RUN dotnet tool install -g Volo.Abp.Cli || true
    ENV PATH="$PATH:/root/.dotnet/tools"
    RUN abp install-libs

    # Build and publish
    WORKDIR /src
    RUN dotnet publish YourCompany.YourProj.Web/YourCompany.YourProj.Web.csproj -c Release -o /app/publish

    FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
    WORKDIR /app
    COPY --from=build /app/publish .
    ENV ASPNETCORE_URLS=http://+:8080
    EXPOSE 8080
    ENTRYPOINT ["dotnet", "YourCompany.YourProj.Web.dll"]
  • CI/CD (DigitalOcean)

    • If you build inside CI, ensure the abp install-libs step runs before dotnet publish, and the working directory is the Web project where package.json/abp.resourcemapping.js live.
    • Do not ignore wwwroot in .dockerignore.
  • Verify at runtime

    • After container starts, check:
      • GET http://your-host/global.css → should return combined CSS (if using Global Assets) or
      • Ensure your page renders <abp-style-bundle name="@BasicThemeBundles.Styles.Global" /> and the network tab shows the CSS request(s) from /styles/global-styles.css included in the bundle.
    • If you still reference “~/global-styles.css” directly anywhere, remove it and rely on the bundle.

Common pitfalls that cause this error

  • Using bundle.AddFiles("~/global-styles.css") with a wrong path. Use a rooted web path like “/styles/global-styles.css” that matches wwwroot/styles/global-styles.css.
  • Missing app.MapAbpStaticAssets() or app.UseStaticFiles() in Program.cs/Module.
  • Not running abp install-libs during container build (relevant if you use theme or library bundles).
  • Publishing from a solution root without first installing libs in the Web project.
  • Trimming/publish options removing embedded/virtual files. If you enabled aggressive trimming, disable it for the web project or ensure ABP assemblies’ resources are preserved.
  1. Sources
  • ABP Basic Theme – add a global style via bundling:
    • https://abp.io/docs/latest/framework/ui/mvc-razor-pages/basic-theme
  • ABP Modularity basics (for where to configure bundling in your module):
    • 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 To make the Angular app request the OpenID Connect discovery document from your real server host instead of https://localhost:44380, you must configure both the Angular environment and the backend Authority/SelfUrl consistently for your deployment host.

Do the following:

  • Angular (environment.prod.ts)
    • Set baseUrl to the public Angular URL you are using in production.
    • Set oAuthConfig.issuer to your backend’s public HTTPS authority (with a trailing slash).
    • Keep requireHttps = true for production (recommended).
    • Example:
import { Environment } from '@abp/ng.core';

  const baseUrl = 'http://serviceip:4200'; // or https if you have TLS on the SPA

  const oAuthConfig = {
    issuer: 'https://serverhost:44380/', // IMPORTANT: public HTTPS authority with trailing slash
    redirectUri: baseUrl,
    clientId: 'NovaLab_App',
    responseType: 'code',
    scope: 'offline_access NovaLab',
    requireHttps: true,
    impersonation: {
      tenantImpersonation: true,
      userImpersonation: true,
    },
  };

  export const environment = {
    production: true,
    application: {
      baseUrl,
      name: 'NovaLab',
    },
    oAuthConfig,
    apis: {
      default: {
        url: 'https://serverhost:44380', // same host as issuer
        rootNamespace: 'RZ.NovaLab',
      },
      AbpAccountPublic: {
        url: oAuthConfig.issuer,
        rootNamespace: 'AbpAccountPublic',
      },
    },
    remoteEnv: {
      url: '/getEnvConfig',
      mergeStrategy: 'deepmerge',
    },
  } as Environment;
  • Backend (HttpApi.Host or AuthServer)
    • Ensure App:SelfUrl equals your public HTTPS root (https://serverhost:44380).
    • Set AuthServer:Authority to the same public HTTPS URL.
    • If you’re behind a reverse proxy/loader (IIS, Nginx, Azure App Service, etc.), make sure the backend generates HTTPS endpoints inside the discovery document. Add forwarded headers and/or force scheme to https:
// Program.cs or the module's OnApplicationInitialization:
  app.UseForwardedHeaders(new ForwardedHeadersOptions
  {
      ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedFor
  });

  // If your proxy doesn’t set X-Forwarded-Proto properly, force https as a fallback:
  app.Use(async (ctx, next) =>
  {
      ctx.Request.Scheme = "https";
      await next();
  });
  • When using OpenIddict, ensure the issuer matches your public HTTPS authority:
PreConfigure<OpenIddictServerBuilder>(builder =>
  {
      builder.SetIssuer(new Uri(configuration["AuthServer:Authority"]!)); // https://serverhost:44380
  });
  • Common pitfalls to avoid
    • Mismatch between Angular issuer and backend Authority/SelfUrl. They must point to the same public host and scheme.
    • Missing trailing slash in issuer on Angular. Add it (…/).
    • Using http in production: Angular’s OAuth library validates that all discovery document URLs are HTTPS when requireHttps is true. Ensure your discovery document (/.well-known/openid-configuration) shows HTTPS for issuer, authorization_endpoint, token_endpoint, jwks_uri, etc. If it shows http, fix proxy headers or force https as shown above.

After these changes, browsing http://serviceip:4200 (or https if applicable) should fetch the discovery document from https://serverhost:44380/.well-known/openid-configuration instead of https://localhost:44380, and complete the OIDC flow successfully.

  1. Sources
  • OpenIddict Angular Migration (issuer trailing slash)
    • https://abp.io/docs/latest/release-info/migration-guides/openiddict-angular
  • Related support threads on ensuring issuer/authority match and HTTPS discovery behind proxies
    • https://abp.io/support/questions/4338
    • https://abp.io/support/questions/10011

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
  • What ABP does (and does not) do

    • When you set AbpClockOptions.Kind = DateTimeKind.Utc, ABP normalizes DateTime values to UTC for persistence and for API responses. This is by design. ABP does not automatically convert DateTime values to the user’s local time in JSON responses. Clients should receive UTC (e.g., 2025-11-25T12:19:28Z) and render/convert to the user time zone on the UI.
    • ABP performs timezone conversions only when you explicitly call the helpers:
      • Server-side: IClock.ConvertToUserTime(...) and IClock.ConvertToUtc(...)
      • JavaScript: abp.clock.normalizeToLocaleString(...) and abp.clock.normalizeToString(...)
      • Blazor: inject IClock and call ConvertToUserTime/ConvertToUtc where you display or accept input.
  • Why your user-specific TimeZone setting did not change the API output

    • The user TimeZone setting (Abp.Timing.TimeZone) affects conversions performed by IClock.*Convert… methods and the ABP UI helpers, not the raw data returned by your API. With UTC clock, API responses stay UTC. You should convert on the UI or on the view model that is specifically meant for display.
  • Why your manual CreationTime change “reverted” after leaving the AppService

    • You are mapping entities to DTOs and returning them. If you mutate a DateTime to local with ToLocalTime(), you produce a local DateTime (Kind=Local/Unspecified). Before serialization, ABP’s JSON serialization keeps it as a DateTime; since the global clock is UTC, and you haven’t used IClock to convert for output, the serializer writes ISO-8601 UTC ending with Z for DateTime by convention. Net result: your manual local conversion is lost in the returned JSON. This is expected; don’t try to store UI-local values into your DTO model. Keep DTOs in UTC and convert for display.
  • Recommended, working setup for your stack (Blazor Server, ABP 9.0.2)

    1. Keep everything in UTC
Configure<AbpClockOptions>(options =>
     {
         options.Kind = DateTimeKind.Utc;
     });
  1. Set the user’s timezone via setting Abp.Timing.TimeZone (e.g., "Turkey Standard Time" or the IANA name depending on your environment).
  2. Blazor Server UI: convert explicitly when showing values and convert back to UTC when saving.
    • Display:
@inject IClock Clock
       @Clock.ConvertToUserTime(dto.CreationTime).ToString("yyyy-MM-dd HH:mm:ss")
 - Input -> Save:
   - If the user picked a DateTime in their local time:
dto.ScheduledTime = Clock.ConvertToUtc(userInputLocalDateTime);
   - If you use DateTimeOffset pickers, prefer storing DateTimeOffset in the entity/DTO where appropriate, or convert via Clock.ConvertToUtc(offset.DateTime) when persisting as DateTime.
  1. If you have MVC/Razor Pages or JS:
    • Add UseAbpTimeZone middleware on the server app to let ABP detect the browser timezone when a per-user/tenant setting is not present.
    • For JS forms using ABP TagHelpers/DatePicker, call before submit:
$form.handleDatepicker('input[type="hidden"][data-hidden-datepicker]');
   This uses abp.clock.normalizeToString under the hood to send UTC values.
 - For displaying in JS (DataTables or custom UI):
abp.clock.normalizeToLocaleString(utcIsoString)
  1. Don’t call DateTime.ToLocalTime() on DTOs you return from services. Instead:
    • Return UTC to clients.
    • Or, if you really must return localized values for a specific endpoint, use IClock.ConvertToUserTime on a dedicated “view”/presentation DTO and mark the property as string (already formatted) or DateTimeOffset with proper offset. Avoid mixing with standard CRUD DTOs which should remain UTC.
  • Quick example (Blazor Server list page)
@inject IClock Clock

  // In the grid template:
  @Clock.ConvertToUserTime(item.CreationTime).ToString("yyyy-MM-dd HH:mm:ss")

  // When creating/updating from local picker value:
  entity.ScheduledOnUtc = Clock.ConvertToUtc(LocalPickerValue);
  • Checklist to solve your issue now
    • Keep AbpClockOptions.Kind = Utc across all apps/services.
    • Ensure the user’s Abp.Timing.TimeZone setting is set (e.g., “Turkey Standard Time”) for that user.
    • In Blazor components, show DateTimes via Clock.ConvertToUserTime; do not expect API to return local times.
    • On create/update, convert UI-picked local times back to UTC with Clock.ConvertToUtc before saving.
    • If you use JS pages, add UseAbpTimeZone middleware and call $form.handleDatepicker(...) on submit.
  1. Sources
  • https://docs.abp.io/en/abp/latest/Framework/Infrastructure/Timing
  • https://abp.io/docs/latest/framework/infrastructure/timing
  • Community article: Developing a Multi-Timezone Application Using the ABP Framework (explains IClock, UseAbpTimeZone, Blazor/JS usage) https://abp.io/community/articles/zk7fnrdq

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
  • Purpose

    • EntityName: Allows a repository instance to target a named entity when the same CLR type is mapped multiple times (e.g., EF Core Shared-type entity types). Default is null; you only set it in advanced scenarios like working on multiple tables with the same entity class.
    • ProviderName: Exposes the underlying data provider used by the repository: “Volo.Abp.EntityFrameworkCore”, “Volo.Abp.MongoDB”, or “Volo.Abp.MemoryDb”. It’s mainly for internal/diagnostic use; you typically don’t set it yourself.
  • Initialization and usage

    • You do not need to initialize either property in a custom base repository unless you have a specific scenario:
      • EntityName: Leave null by default. If you need it, set it just before using the repository (e.g., in your application/domain service) and then execute queries/commands. Example:
public class MyService : ApplicationService
      {
          private readonly IRepository<MySharedType, Guid> _repo;

          public MyService(IRepository<MySharedType, Guid> repo)
          {
              _repo = repo;
          }

          public async Task<List<MySharedType>> GetFromProductTableAsync()
          {
              _repo.SetEntityName("Product"); // select the named entity mapping
              return await _repo.GetListAsync();
          }
      }
  Note: The default is null; only set a name if you intentionally mapped the same CLR type as different named entities in your DbContext/model configuration.
- ProviderName: It’s read-only from consumer perspective (you just read it). The framework sets it in the concrete repository implementation. You can check it when you need provider-specific logic:
if (_repo.ProviderName == Volo.Abp.EntityFrameworkCore.AbpEfCoreConsts.ProviderName)
      {
          // EF Core-specific path
      }
  • In custom repository base classes (like your ReadOnlyBasicRepositoryBase<TEntity>):

    • Do not assign ProviderName; allow the specific provider implementation (EF Core/MongoDB/MemoryDb) to supply it.
    • Keep EntityName as null unless your base class implements behavior depending on it. Typically, you won’t set it there; consumers can call SetEntityName(...) before use.
  • Minimal implementation suggestions for your base class

public abstract class ReadOnlyBasicRepositoryBase<TEntity> :
      IReadOnlyBasicRepository<TEntity>,
      IRepository,
      IServiceProviderAccessor,
      IUnitOfWorkEnabled
      where TEntity : class, IEntity
  {
      // The framework/provider sets this. Expose it as required by the interface.
      public string ProviderName { get; protected set; }

      // Default null. Let consumers set it when needed via SetEntityName.
      public string EntityName { get; protected set; }

      public virtual void SetEntityName(string entityName)
      {
          EntityName = entityName;
      }

      // ...rest of your implementation
  }
  • If your interface requires get-only for ProviderName, implement a protected setter or assign it in the provider-specific derived class. For EF Core/Mongo/MemDb repositories supplied by ABP, this is already handled. If you’re implementing a fully custom provider, set ProviderName with your constant provider identifier.

  • When to set them in lifecycle

    • ProviderName: Already initialized by the concrete repository instance when resolved from DI; you just read it.
    • EntityName: Set by the caller right before executing repository methods that depend on a specific named mapping. Otherwise, leave it null.
  1. Sources
  • https://abp.io/docs/latest/framework/architecture/domain-driven-design/repositories
  • https://abp.io/docs/latest/release-notes
  • 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.

Showing 251 to 260 of 898 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.2.0-preview. Updated on February 17, 2026, 09:10
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.