Sending transactional email from ASP.NET with Envello
System.Net.Mail.SmtpClient has been deprecated in .NET for years, and even when it worked it gave you no delivery visibility: no bounce events, no complaint handling, no way to check whether a message actually reached the inbox. For transactional email in production ASP.NET applications, a direct HTTP call to an email API is the modern approach.
This guide shows how to send transactional email from an ASP.NET Core application using Envello's REST API with HttpClient and proper dependency injection.
Prerequisites
An ASP.NET Core 8+ project, an Envello account with a verified sending domain, and your API key in configuration (appsettings.json or environment variables, not hardcoded).
Configuration
Add your Envello settings to appsettings.json and bind them to a strongly-typed options class.
- Create an EnvelloOptions class with properties: ApiKey, BaseUrl (https://api.envello.dev), DefaultFrom
- Register in Program.cs: builder.Services.Configure<EnvelloOptions>(builder.Configuration.GetSection("Envello"))
- Store the API key in user secrets (dotnet user-secrets) for local development and in environment variables for production
HttpClient setup with IHttpClientFactory
Use IHttpClientFactory to manage the HttpClient lifecycle. This avoids socket exhaustion (the classic .NET HttpClient pitfall) and gives you proper connection pooling.
- Register a named or typed client in Program.cs: builder.Services.AddHttpClient<IEmailService, EnvelloEmailService>()
- Configure the base address and default headers in the factory registration
- The factory handles DNS refresh and connection pooling automatically
Create the email service
Implement the service as a class that takes HttpClient via constructor injection (typed client pattern) and IOptions<EnvelloOptions> for configuration.
- Define an IEmailService interface with a Task<SendResult> SendAsync(EmailMessage message) method
- Implement EnvelloEmailService with the HttpClient and options injected
- Build the JSON payload with System.Text.Json: new { from, to, subject, html, text }
- POST to /emails with the Authorization header set to Bearer + API key
- Deserialize the response to get the message ID for logging
Error handling and resilience
Use Polly (via Microsoft.Extensions.Http.Resilience) to add retry policies to the HttpClient. This handles transient failures (network timeouts, 5xx responses, 429 rate limits) without custom retry loops in your application code.
- Add the resilience NuGet package: Microsoft.Extensions.Http.Resilience
- Configure retry on the HttpClient registration: .AddStandardResilienceHandler()
- The standard handler retries on 5xx and 408 with exponential backoff by default
- For 429 (rate limit), read the Retry-After header and wait that duration before retrying
- For 400 (validation error), don't retry; log the error body and surface it to the caller
- For 401/403 (auth error), don't retry; log and alert, the API key is likely invalid or expired
Async all the way down
ASP.NET Core is async by design. Keep the entire email sending path async to avoid thread pool starvation under load.
Use await throughout: HttpClient.PostAsync, ReadAsStringAsync, JsonSerializer.DeserializeAsync. Never call .Result or .Wait() on the task, which blocks a thread pool thread and can deadlock under high concurrency.
For fire-and-forget scenarios (sending a welcome email where the caller doesn't need to know the outcome), use a background service or IHostedService with a Channel<EmailMessage> as a queue. This decouples the web request from the email send entirely.
Background sending with IHostedService
For high-throughput applications, queue email sends to a background service instead of sending inline with the request.
- Create a Channel<EmailMessage> and register it as a singleton
- The controller or service writes to the channel: await channel.Writer.WriteAsync(message)
- A BackgroundService reads from the channel and sends via the email service
- The background service can batch or throttle sends to stay within rate limits
- If the application shuts down with messages in the queue, the hosted service's StopAsync can drain remaining messages with a timeout
Rendering email content with Razor
ASP.NET Core's Razor engine can render email templates the same way it renders web pages. Use RazorViewEngine to render a .cshtml template to a string and pass the result as the html field.
Create templates in a Views/Emails/ directory. Use a strongly-typed model (PasswordResetModel, OrderConfirmationModel) so the template gets compile-time checking. Render with IRazorViewEngine.FindView and IViewBufferScope, or use a library like RazorLight that simplifies the rendering pipeline.
For simpler messages, string interpolation with a raw string literal works fine. A password reset email doesn't need a full Razor template.
Webhook endpoint
Add a controller endpoint to receive Envello's delivery event webhooks.
- Create a [ApiController] with a POST endpoint at /webhooks/envello
- Read the raw request body and the X-Envello-Signature header
- Compute HMAC-SHA256 of the raw body using your webhook signing secret and compare with the signature
- Reject requests with invalid signatures (return 401)
- Parse the event payload and handle: delivered (log), bounced (suppress address), complained (suppress and flag)
- Return 200 immediately; process asynchronously if the handler does database work
Testing
For unit tests, mock IHttpMessageHandler (or use MockHttpMessageHandler from the RichardSzalay.MockHttp package) to verify the request payload without making real HTTP calls.
Verify: the Authorization header contains the correct API key, the Content-Type is application/json, the request body contains the expected recipient and subject, and the service correctly handles 2xx, 4xx, and 5xx responses.
For integration tests, use Envello's test mode to validate the full flow: API call accepted, webhook events fire, but no real email is delivered.
Production checklist
Before deploying to production, verify the following.
- API key is in environment variables or a secrets manager, not in appsettings.json committed to source control
- HttpClient uses IHttpClientFactory, not new HttpClient()
- Retry policy handles 429 and 5xx responses with backoff
- Webhook endpoint validates the HMAC signature on every request
- Bounce and complaint events suppress the address in your application's database
- Email rendering uses parameterized templates, not string concatenation with user input (XSS risk in HTML email)
- Recipient email addresses are not logged; use message IDs for tracing
Check your domain's SPF, DKIM, and DMARC records
Paste in a domain and see what's missing, plus the exact DNS records to fix it. Free, no account needed.
Check your domain →