Fix it with the following steps:
A) Ensure email sending is enabled in Production
public override void ConfigureServices(ServiceConfigurationContext context)
{
#if DEBUG
context.Services.Replace(ServiceDescriptor.Singleton<IEmailSender, NullEmailSender>());
#endif
}
"Settings": {
"Abp.Mailing.Smtp.Host": "smtp.yourprovider.com",
"Abp.Mailing.Smtp.Port": "587",
"Abp.Mailing.Smtp.UserName": "user",
"Abp.Mailing.Smtp.Password": "<ENCRYPTED_PASSWORD>",
"Abp.Mailing.Smtp.EnableSsl": "true",
"Abp.Mailing.Smtp.UseDefaultCredentials": "false",
"Abp.Mailing.DefaultFromAddress": "no-reply@yourdomain.com",
"Abp.Mailing.DefaultFromDisplayName": "Your App"
}
Note: The SMTP password must be stored encrypted if you put it in settings. Use IStringEncryptionService to encrypt before saving to the DB or write a small snippet at startup to call SettingManager.SetGlobalAsync for the password (ABP encrypts on set, decrypts on get).
B) Stop loading templates from the physical file system; embed and use ABP VFS + ITemplateRenderer Right now, LoadTemplate reads files via File.ReadAllTextAsync from AppContext.BaseDirectory/Emailing/Templates. This commonly breaks in containers, single-file publish, or when paths differ.
Use ABP’s text templating + virtual file system:
[DependsOn(typeof(AbpEmailingModule))]
public class YourProjectDomainModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<YourProjectDomainModule>();
});
}
}
public static class MyEmailTemplates
{
public const string UserCreation = "MyEmailTemplates.UserCreation";
public const string ChangePassword = "MyEmailTemplates.ChangePassword";
}
public class EmailTemplateDefinitionProvider : TemplateDefinitionProvider
{
public override void Define(ITemplateDefinitionContext context)
{
context.Add(
new TemplateDefinition(MyEmailTemplates.UserCreation)
.WithVirtualFilePath("/Emailing/Templates/UserCreation.tpl", isInlineLocalized: true)
);
context.Add(
new TemplateDefinition(MyEmailTemplates.ChangePassword)
.WithVirtualFilePath("/Emailing/Templates/ChangePassword.tpl", isInlineLocalized: true)
);
}
}
private async Task<string> RenderEmailBodyAsync(string templateName, object model)
{
// Map friendly names to your template keys
var templateKey = templateName.Equals("UserCreation.tpl", StringComparison.OrdinalIgnoreCase)
? MyEmailTemplates.UserCreation
: MyEmailTemplates.ChangePassword;
return await _templateRenderer.RenderAsync(templateKey, model);
}
C) If you want to keep raw HTML templates and simple Replace (without Scriban)
private readonly IFileProvider _fileProvider;
public SendGridEmailNotificationAppServiceBase(..., IVirtualFileProvider vfs, ...)
{
_fileProvider = vfs;
}
private async Task<string> LoadTemplateFromVfsAsync(string virtualPath)
{
var file = _fileProvider.GetFileInfo(virtualPath); // e.g. "/Emailing/Templates/UserCreation.tpl"
if (!file.Exists) throw new FileNotFoundException($"Template not found: {virtualPath}");
using var stream = file.CreateReadStream();
using var reader = new StreamReader(stream);
return await reader.ReadToEndAsync();
}
D) Ensure environment-specific URLs are configured
E) Validate multi-tenancy handling and encryption
F) Use MailKit in production (recommended) If you see warnings about SmtpClient, add AbpMailKit and configure MailKit sender:
G) Quick checklist for deployed environment
Code summary (minimal changes to your class):
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
public class OrderPaidEvent : EntityEventData<Order>
{
public OrderPaidEvent(Order entity) : base(entity) {}
}
- Event handler to grant access:
public class OrderPaidEventHandler : ILocalEventHandler<OrderPaidEvent>, ITransientDependency
{
private readonly IRepository<Enrollment, Guid> _enrollmentRepo;
public OrderPaidEventHandler(IRepository<Enrollment, Guid> enrollmentRepo)
{
_enrollmentRepo = enrollmentRepo;
}
public async Task HandleEventAsync(OrderPaidEvent eventData)
{
var order = eventData.Entity;
foreach (var item in order.Items)
{
await _enrollmentRepo.InsertAsync(new Enrollment(order.UserId, item.CourseId), autoSave: true);
}
}
}
- Guarding course content:
public class CourseAppService : ApplicationService
{
private readonly IRepository<Enrollment, Guid> _enrollmentRepo;
private readonly IRepository<Course, Guid> _courseRepo;
public async Task<CourseDto> GetAsync(Guid courseId)
{
var isEnrolled = await _enrollmentRepo.AnyAsync(e => e.UserId == CurrentUser.GetId() && e.CourseId == courseId);
if (!isEnrolled) throw new AbpAuthorizationException("Not enrolled");
var course = await _courseRepo.GetAsync(courseId);
return ObjectMapper.Map<Course, CourseDto>(course);
}
}
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
[DependsOn(
typeof(MyDocApplicationModule),
typeof(MyDocHttpApiModule),
typeof(MyDocEntityFrameworkCoreModule)
)]
public class HostAppHttpApiHostModule : AbpModule
{
}
4) Add the module’s EF Core Configure call into each host’s migrations DbContext (or migrations project) so the host’s DbMigrator migrates the module tables:
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ConfigureMyDoc(); // your module's extension method
}
5) Run each host’s DbMigrator to create/update databases.
This is the recommended way to reuse functionality across multiple ABP applications (module as a package rather than project reference), keeping hosts loosely coupled.
Generating Angular proxies without ABP Suite entities
Database segregation approaches when reusing the same module in two hosts
Configure<AbpDbContextOptions>(options =>
{
options.Configure<MyDocDbContext>(opt => { opt.UseSqlServer(); /* or UseMySQL etc. */ });
});
- This allows Host 1 and Host 2 to point the module to different databases or even different DBMS per host while sharing the same binary module. See also the note below for multiple providers per solution.
Deployment separation
Notes and tips
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
Use the correct bundling modules on the host (server) project
Example:
[DependsOn(
typeof(AbpAutofacModule),
typeof(AbpAspNetCoreMvcUiBundlingModule),
typeof(AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeBundlingModule)
// + any commercial WebAssembly bundling modules you use, e.g.:
// typeof(AbpAuditLoggingBlazorWebAssemblyBundlingModule)
)]
public class MyProjectNameBlazorModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
// Add services to the container
context.Services.AddRazorComponents()
.AddInteractiveWebAssemblyComponents();
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
var env = context.GetEnvironment();
if (env.IsDevelopment())
{
app.UseWebAssemblyDebugging();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
// IMPORTANT: maps static/virtual assets including global.js & global.css
app.MapAbpStaticAssets();
app.UseRouting();
app.UseAntiforgery();
app.UseConfiguredEndpoints(builder =>
{
builder.MapRazorComponents<App>()
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(WebAppAdditionalAssembliesHelper.GetAssemblies<MyProjectNameBlazorClientModule>());
});
}
}
public class MonacoBundleScriptContributor : BundleContributor
{
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddIfNotContains("_content/BlazorMonaco/jsInterop.js");
context.Files.AddIfNotContains("_content/BlazorMonaco/lib/monaco-editor/min/vs/loader.js");
context.Files.AddIfNotContains("_content/BlazorMonaco/lib/monaco-editor/min/vs/editor/editor.main.js");
}
}
// In the same Blazor.Client project (e.g., in your module)
private void ConfigureBundles()
{
Configure<AbpBundlingOptions>(options =>
{
var globalScripts = options.ScriptBundles.Get(BlazorWebAssemblyStandardBundles.Scripts.Global);
globalScripts.AddContributors(typeof(MonacoBundleScriptContributor));
});
}
Notes:
Use AddIfNotContains and relative paths starting with _content/ for library static files.
Ensure the Blazor.Client project references the BlazorMonaco package so those _content files exist at runtime.
Reference the client project from the host project
Ensure MapAbpStaticAssets is called in the host pipeline
Verify at runtime
Common pitfalls to check
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
Key points
Step-by-step
A) Install and wire modules
/Payment/GatewaySelection/Payment/Stripe/PrePaymentB) Configure Payment and SaaS
"Payment": {
"Stripe": {
"PublishableKey": "pk_test_xxx",
"SecretKey": "sk_test_xxx",
"WebhookSecret": "whsec_xxx",
"Currency": "USD", // set as needed
"Locale": "auto",
"PaymentMethodTypes": [] // leave empty; module adds “card” automatically
}
}
public override void PreConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
Configure<PaymentWebOptions>(options =>
{
options.RootUrl = configuration["App:SelfUrl"];
options.CallbackUrl = configuration["App:SelfUrl"] + "/PaymentSucceed";
});
}
C) Define SaaS Editions, Plans and Stripe mapping
D) Self-service “change plan” UI flow (tenant-facing)
Example MVC page model (tenant UI):
public class ChangePlanModel : PageModel
{
private readonly IPaymentRequestAppService _paymentRequestAppService;
private readonly ICurrentTenant _currentTenant;
public ChangePlanModel(IPaymentRequestAppService paymentRequestAppService, ICurrentTenant currentTenant)
{
_paymentRequestAppService = paymentRequestAppService;
_currentTenant = currentTenant;
}
public async Task<IActionResult> OnPostAsync(Guid editionId, Guid planId)
{
// Carry needed info for webhook/finalization
var pr = await _paymentRequestAppService.CreateAsync(
new PaymentRequestCreateDto
{
Products =
{
new PaymentRequestProductCreateDto
{
PaymentType = PaymentType.Subscription,
Name = "PlanChange",
Code = $"{_currentTenant.Id}_{planId}",
Count = 1,
PlanId = planId
}
}
}.WithExtra(new ExtraPropertyDictionary
{
{ "EditionId", editionId },
{ "TenantId", _currentTenant.Id }
})
);
return LocalRedirectPreserveMethod("/Payment/GatewaySelection?paymentRequestId=" + pr.Id);
}
}
Helper extension (optional):
public static class PaymentRequestDtoExtensions
{
public static PaymentRequestCreateDto WithExtra(this PaymentRequestCreateDto dto, ExtraPropertyDictionary extras)
{
dto.ExtraProperties = extras;
return dto;
}
}
Notes
E) Redirects and built-in UI
F) Webhooks are mandatory
G) Downgrade and upgrade are the same flow
H) If you prefer ISubscriptionAppService
I) Angular UI
Common pitfalls and fixes
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
Do the following:
"App": {
"SelfUrl": "https://10.136.36.151:44380",
"RedirectAllowedUrls": "http://localhost:4200,http://10.136.36.151:4200,https://10.136.36.151:4200"
}
Notes:
Include every URL the SPA can use: local dev, IP:port, domain names, and HTTPS variants (if applicable).
If you terminate TLS elsewhere but serve Angular over HTTPS, include the https:// variant.
Restart the AuthServer after changing appsettings.
Ensure the OpenIddict client (NovaLab_App) has the exact redirect URIs
Align Angular environment configuration
Don’t mix schemes unintentionally
Reverse proxy/containers tip
After these changes, restart services. The login should proceed without “invalid redirect_uri”.
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
Apply all steps below:
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Bundling;
public class ProdigyEnablerWebModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpBundlingOptions>(options =>
{
options.StyleBundles.Configure(BasicThemeBundles.Styles.Global, bundle =>
{
// Path must be relative to web root (wwwroot)
bundle.AddFiles("/styles/global-styles.css");
});
});
}
}
Notes:
<abp-style-bundle name="@BasicThemeBundles.Styles.Global" />
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
var env = context.GetEnvironment();
if (!env.IsDevelopment())
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.MapAbpStaticAssets(); // important for ABP’s virtual files
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseConfiguredEndpoints();
}
Ensure client libraries and bundles are present when you build/publish:
Dockerfile (reference pattern)
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
# Copy solution and restore
COPY ./ ./
RUN dotnet restore
# Install ABP libs for the Web project (adjust path to your Web csproj folder)
WORKDIR /src/YourCompany.YourProj.Web
RUN dotnet tool restore
RUN dotnet tool install -g Volo.Abp.Cli || true
ENV PATH="$PATH:/root/.dotnet/tools"
RUN abp install-libs
# Build and publish
WORKDIR /src
RUN dotnet publish YourCompany.YourProj.Web/YourCompany.YourProj.Web.csproj -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
ENTRYPOINT ["dotnet", "YourCompany.YourProj.Web.dll"]
CI/CD (DigitalOcean)
Verify at runtime
Common pitfalls that cause this error
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
Do the following:
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;
// 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();
});
PreConfigure<OpenIddictServerBuilder>(builder =>
{
builder.SetIssuer(new Uri(configuration["AuthServer:Authority"]!)); // https://serverhost:44380
});
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.
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 ABP does (and does not) do
Why your user-specific TimeZone setting did not change the API output
Why your manual CreationTime change “reverted” after leaving the AppService
Recommended, working setup for your stack (Blazor Server, ABP 9.0.2)
Configure<AbpClockOptions>(options =>
{
options.Kind = DateTimeKind.Utc;
});
@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.
$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)
@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);
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.
Purpose
Initialization and usage
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>):
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
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.