Katalyst.Core.Email
1.2.1
dotnet add package Katalyst.Core.Email --version 1.2.1
NuGet\Install-Package Katalyst.Core.Email -Version 1.2.1
<PackageReference Include="Katalyst.Core.Email" Version="1.2.1" />
<PackageVersion Include="Katalyst.Core.Email" Version="1.2.1" />
<PackageReference Include="Katalyst.Core.Email" />
paket add Katalyst.Core.Email --version 1.2.1
#r "nuget: Katalyst.Core.Email, 1.2.1"
#:package Katalyst.Core.Email@1.2.1
#addin nuget:?package=Katalyst.Core.Email&version=1.2.1
#tool nuget:?package=Katalyst.Core.Email&version=1.2.1
Katalyst Email
.NET library for sending transactional email through Resend using the official Resend .NET SDK. Configuration binds Email (provider, shared defaults, optional Razor base path, optional queue hints) and Email:Resend (API key), then registers Core.Email.Abstractions.IEmailSender.
The package is structured for additional providers later; today only Resend is implemented.
Requirements
- .NET 10 SDK
- A Resend account and API key
Installation
Add the Katalyst.Core.Email package to your app (NuGet feed or a project reference to Email/Core.Email/Core.Email.csproj).
dotnet add package Katalyst.Core.Email
Resend and domain setup
Before the library can send mail, configure Resend and your sending domain.
- Create an API key in the Resend dashboard (API Keys). Use a key with permission to send email.
- Add and verify a domain in Resend (Domains). Complete DNS records (SPF, DKIM, etc.) until the domain shows as verified.
- Choose a “from” address that uses your verified domain, for example
noreply@mail.example.com. Resend will reject or fail sends if the domain is not verified or the address is not allowed for your account. - Optional — templates for
SendTemplatedAsync: create a template in Resend, publish it, and copy its template UUID from the dashboard.
Official help: Resend documentation.
Configuration
Bindings use two layers:
Email— provider selection and settings shared across vendors (EmailOptions.SectionName).Email:Resend— Resend-specific options (ResendEmailOptions.SectionName).
| Setting | Section | Description |
|---|---|---|
Provider |
Email |
Must be Resend (other values throw at startup). |
DefaultFrom:Address |
Email |
Default sender email (must align with your verified domain). |
DefaultFrom:Name |
Email |
Optional display name for the default sender. |
RazorTemplateBasePath |
Email |
Optional base folder for relative .cshtml paths used by SendRazorTemplatedAsync. Relative paths resolve under this folder, then to an absolute path from the app base directory when needed. |
Queue:Enabled |
Email |
When true, send methods do not call Resend. They return EmailResult with status queued (see EmailConstants.EmailStatus.Queued) and optional BodyContent / BodyIsHtml so your host app can enqueue the payload. Default false. |
Queue:QueueName |
Email |
Optional hint for your queue (logged only; Core.Email does not enqueue). |
ApiKey |
Email:Resend |
Resend API key (re_...) or an encrypted value produced by Email.Utility encrypt (see below). |
IsApiKeyEncrypted |
Email:Resend |
Set to false when ApiKey is the plaintext value from the dashboard. When omitted or not false, the runtime attempts to decrypt ApiKey for use with stored ciphertext. |
Example appsettings.json:
{
"Email": {
"Provider": "Resend",
"DefaultFrom": {
"Address": "noreply@mail.yourdomain.com",
"Name": "Your App"
},
"RazorTemplateBasePath": "Templates",
"Queue": {
"Enabled": false,
"QueueName": "outbound-email"
},
"Resend": {
"ApiKey": "re_xxxxxxxxxxxxxxxxxxxxxxxx",
"IsApiKeyEncrypted": false
}
}
}
Environment variables override JSON when both are loaded (the development tool merges environment variables after the file). Examples:
Email__ProviderEmail__DefaultFrom__AddressEmail__DefaultFrom__NameEmail__RazorTemplateBasePathEmail__Queue__EnabledEmail__Queue__QueueNameEmail__Resend__ApiKeyEmail__Resend__IsApiKeyEncrypted
ASP.NET Core applications
Use the same Email configuration as in Configuration (typically appsettings.json plus User Secrets or environment variables for the API key in development).
Register services (Program.cs)
With the minimal hosting model, register email right after WebApplication.CreateBuilder:
using Core.Email;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(); // if you use MVC controllers
builder.Services.AddAppEmail(builder.Configuration);
var app = builder.Build();
// …middleware and endpoints…
app.Run();
Options are validated on startup (ValidateOnStart()); a missing or invalid Email:Provider / Resend settings causes the host to fail fast when the service provider is built.
If you still use Startup, call services.AddAppEmail(Configuration) from ConfigureServices.
Example: minimal API endpoint
using Core.Email.Abstractions;
using Core.Email.Models;
app.MapPost("/notify/welcome", async (string to, IEmailSender emailSender, CancellationToken cancellationToken) =>
{
var message = new RegularMail
{
Subject = "Welcome",
To = [new EmailContact(to)],
Body = "<p>Thanks for signing up.</p>",
IsHtml = true,
};
var result = await emailSender.SendAsync(message, cancellationToken);
if (result.Succeeded)
return Results.Ok();
if (result.IsQueued)
return Results.Accepted(); // enqueue using result.BodyContent in your app
return Results.BadRequest(new { result.Status, result.Reason });
});
Example: MVC API controller
using Core.Email.Abstractions;
using Core.Email.Models;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public sealed class NotificationsController : ControllerBase
{
private readonly IEmailSender _emailSender;
public NotificationsController(IEmailSender emailSender) =>
_emailSender = emailSender;
[HttpPost("welcome")]
public async Task<IActionResult> SendWelcome(
[FromQuery] string to,
CancellationToken cancellationToken)
{
var result = await _emailSender.SendAsync(
new RegularMail
{
Subject = "Welcome",
To = [new EmailContact(to)],
Body = "<p>Thanks for signing up.</p>",
IsHtml = true,
},
cancellationToken);
return result.Succeeded ? Ok()
: result.IsQueued ? Accepted()
: BadRequest(new { result.Status, result.Reason });
}
}
Each send returns EmailResult with Status (see EmailConstants.EmailStatus), optional Reason (provider API error text on failure), the original Message, and when applicable BodyContent / BodyIsHtml (plain or Razor body). Use Succeeded, IsQueued, or compare Status instead of relying on exceptions for normal outcomes.
Using the library (step by step)
Add configuration — Add the
Emailsection withProvider,DefaultFrom, nestedResend(ApiKey, andIsApiKeyEncryptedif using plaintext keys). Optionally setQueueif your host enqueues outbound mail itself.Register services — In
Program.cs(minimal host) orStartup.ConfigureServices, callAddAppEmailwith the rootIConfiguration:using Core.Email; builder.Services.AddAppEmail(builder.Configuration);Inject
IEmailSender— ResolveCore.Email.Abstractions.IEmailSenderin constructors, minimal API parameters, or[FromServices]where appropriate.Build a message — Use
RegularMailfor plain HTML/text,TemplateMailfor a published Resend template (template id must be a GUID string), orRazorMailfor a local.cshtmltemplate rendered by RazorLight.Send — Call
SendAsync,SendTemplatedAsync, orSendRazorTemplatedAsync. For plain sends,Body,Subject, and at least oneTorecipient are required (useIsHtmlto choose HTML vs plain text).
If you omit From on a message, the sender defaults to Email:DefaultFrom.
When Email:Queue:Enabled is true, the library skips the provider call and returns EmailResult.Queued so you can persist or enqueue BodyContent (and honor BodyIsHtml) in your application.
Examples
Namespaces: Core.Email.Abstractions, Core.Email.Models.
Plain HTML and text email
using Core.Email.Abstractions;
using Core.Email.Models;
var message = new RegularMail
{
Subject = "Welcome",
To = [new EmailContact("user@example.com", "User Name")],
Body = "<p>Welcome to the <strong>app</strong>.</p>",
IsHtml = true,
};
var result = await emailSender.SendAsync(message);
Custom from, CC, and BCC
var message = new RegularMail
{
Subject = "Invoice ready",
To = [new EmailContact("customer@example.com")],
Body = "Your invoice is attached in the portal.",
From = new EmailContact("billing@mail.yourdomain.com", "Billing"),
Cc = [new EmailContact("finance@yourdomain.com")],
Bcc = [new EmailContact("archive@yourdomain.com")],
};
var result = await emailSender.SendAsync(message);
Resend template (published template UUID)
Resend supplies subject and bodies from the template. TemplateMail still requires Subject on BaseMail for the model; use an empty subject — the Resend implementation sets the outbound Subject to empty on the SDK message because the template defines visible content.
var message = new TemplateMail
{
Subject = string.Empty,
TemplateId = "00000000-0000-0000-0000-000000000000",
To = [new EmailContact("user@example.com")],
TemplateVariables = new Dictionary<string, object>
{
["name"] = "Alex",
["resetLink"] = "https://app.example.com/reset?token=...",
},
};
var result = await emailSender.SendTemplatedAsync(message);
Replace the GUID with your template’s id from the Resend dashboard.
Local Razor .cshtml template (RazorLight)
dynamic model = new
{
name = "Alex",
resetLink = "https://app.example.com/reset?token=...",
};
var message = new RazorMail
{
Subject = "Welcome",
To = [new EmailContact("user@example.com")],
TemplatePath = "Welcome.cshtml",
Data = model,
};
var result = await emailSender.SendRazorTemplatedAsync(message);
Set Email:RazorTemplateBasePath when using relative template paths (they combine with that folder, then resolve to an absolute path). You can also pass a TemplatePath value that is already an absolute file path.
The host registers a singleton IRazorLightEngine with UseFileSystemProject(AppContext.BaseDirectory) and UseMemoryCachingProvider(). After resolution, templates are rendered with CompileRenderAsync(absoluteTemplatePath, model) so compilation and caching stay centralized in RazorLight.
Solution layout
Email/Core.Email— packable library (NuGet package id:Katalyst.Core.Email): abstractions, models, Resend provider, Razor rendering, optional API-key decryption helpers.Email/Email.Utility— development CLI (encrypt,decrypt, send smoke tests; not published as a package).Email/Email.slnx— solution file.
Email.Utility sample appsettings.json
The CLI loads Email/Email.Utility/appsettings.json next to the built assembly by default (see Email.Utility README). Structure matches Email binding plus optional Serilog for console logging:
{
"Serilog": {
"Using": [ "Serilog.Sinks.Console" ],
"MinimumLevel": "Information",
"WriteTo": [ { "Name": "Console" } ]
},
"Email": {
"Provider": "Resend",
"Queue": {
"Enabled": false,
"QueueName": "app:emails"
},
"DefaultFrom": {
"Address": "noreply@mail.yourdomain.com",
"Name": "Your App"
},
"RazorTemplateBasePath": "Templates",
"Resend": {
"ApiKey": "<paste ciphertext>"
}
}
}
Use ciphertext as shown (omit IsApiKeyEncrypted so the library decrypts), or for local-only testing set "ApiKey": "re_..." and "IsApiKeyEncrypted": false.
dotnet build Email/Email.slnx
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.Caching.Memory (>= 10.0.0)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Http (>= 10.0.0)
- Microsoft.Extensions.Logging (>= 10.0.0)
- Microsoft.Extensions.Options (>= 10.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.0)
- Microsoft.Extensions.Options.DataAnnotations (>= 10.0.0)
- RazorLight (>= 2.3.1)
- Resend (>= 0.5.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.