Back to Blog

How to Send Emails in C# / .NET (ASP.NET Core, 2026 Guide)

13 min read

.NET has SmtpClient built in, but Microsoft themselves marked it as obsolete. The recommended approach is to use a third-party library for SMTP or, better yet, an API-based email provider.

This guide covers how to send emails from ASP.NET Core using API providers. HttpClient calls, dependency injection, Razor templates, and background sending with hosted services. All examples use .NET 8+ and C# 12.

SmtpClient vs API Providers

// SmtpClient - obsolete, don't use
using var client = new SmtpClient("smtp.gmail.com", 587);
client.Credentials = new NetworkCredential("you@gmail.com", "password");
await client.SendMailAsync(message);
// [Obsolete] SmtpClient doesn't support modern protocols
 
// API provider: one HTTP call
await httpClient.PostAsJsonAsync(
    "https://api.sequenzy.com/v1/transactional/send",
    new { to, subject, body }
);

Pick a Provider

  • Sequenzy is built for SaaS. Transactional emails, marketing campaigns, automated sequences from one API. Native Stripe integration.
  • Resend is developer-friendly. Clean API. They have one-off broadcast campaigns but no automations or sequences.
  • SendGrid is the enterprise option. Has an official .NET SDK. Good for high volume.

Install

Terminal
# No SDK needed - uses built-in HttpClient
Terminal
dotnet add package Resend
Terminal
dotnet add package SendGrid

Add your API key to appsettings.json:

appsettings.json
{
"Email": {
  "Provider": "sequenzy",
  "ApiKey": "sq_your_api_key_here"
}
}
appsettings.json
{
"Email": {
  "Provider": "resend",
  "ApiKey": "re_your_api_key_here"
}
}
appsettings.json
{
"Email": {
  "Provider": "sendgrid",
  "ApiKey": "SG.your_api_key_here"
}
}

Create an Email Service

Services/EmailService.cs
using System.Net.Http.Headers;
using System.Text.Json;

public interface IEmailService
{
  Task SendAsync(string to, string subject, string body);
}

public class SequenzyEmailService : IEmailService
{
  private readonly HttpClient _http;

  public SequenzyEmailService(HttpClient http, IConfiguration config)
  {
      _http = http;
      _http.BaseAddress = new Uri("https://api.sequenzy.com/v1/");
      _http.DefaultRequestHeaders.Authorization =
          new AuthenticationHeaderValue("Bearer", config["Email:ApiKey"]);
  }

  public async Task SendAsync(string to, string subject, string body)
  {
      var response = await _http.PostAsJsonAsync("transactional/send", new
      {
          to,
          subject,
          body,
      });
      response.EnsureSuccessStatusCode();
  }
}
Services/EmailService.cs
using Resend;

public interface IEmailService
{
  Task SendAsync(string to, string subject, string html);
}

public class ResendEmailService : IEmailService
{
  private readonly IResend _resend;

  public ResendEmailService(IResend resend)
  {
      _resend = resend;
  }

  public async Task SendAsync(string to, string subject, string html)
  {
      await _resend.EmailSendAsync(new EmailMessage
      {
          From = "Your App <noreply@yourdomain.com>",
          To = to,
          Subject = subject,
          HtmlBody = html,
      });
  }
}
Services/EmailService.cs
using SendGrid;
using SendGrid.Helpers.Mail;

public interface IEmailService
{
  Task SendAsync(string to, string subject, string html);
}

public class SendGridEmailService : IEmailService
{
  private readonly ISendGridClient _client;

  public SendGridEmailService(ISendGridClient client)
  {
      _client = client;
  }

  public async Task SendAsync(string to, string subject, string html)
  {
      var from = new EmailAddress("noreply@yourdomain.com", "Your App");
      var toAddress = new EmailAddress(to);
      var msg = MailHelper.CreateSingleEmail(from, toAddress, subject, null, html);

      var response = await _client.SendEmailAsync(msg);
      if (!response.IsSuccessStatusCode)
      {
          throw new Exception($"SendGrid returned {response.StatusCode}");
      }
  }
}

Register in DI:

Program.cs
builder.Services.AddHttpClient<IEmailService, SequenzyEmailService>();
Program.cs
builder.Services.AddOptions();
builder.Services.AddHttpClient<ResendClient>();
builder.Services.Configure<ResendClientOptions>(o =>
  o.ApiToken = builder.Configuration["Email:ApiKey"]!);
builder.Services.AddTransient<IResend, ResendClient>();
builder.Services.AddTransient<IEmailService, ResendEmailService>();
Program.cs
builder.Services.AddSingleton<ISendGridClient>(
  new SendGridClient(builder.Configuration["Email:ApiKey"]));
builder.Services.AddTransient<IEmailService, SendGridEmailService>();

Minimal API Endpoint

// Program.cs
app.MapPost("/api/email/send-welcome", async (
    WelcomeRequest request,
    IEmailService email) =>
{
    await email.SendAsync(
        request.Email,
        $"Welcome, {request.Name}",
        $"<h1>Welcome, {request.Name}</h1><p>Your account is ready.</p>"
    );
    return Results.Ok(new { sent = true });
});
 
record WelcomeRequest(string Email, string Name);

Or with a controller:

[ApiController]
[Route("api/[controller]")]
public class EmailController : ControllerBase
{
    private readonly IEmailService _email;
 
    public EmailController(IEmailService email) => _email = email;
 
    [HttpPost("send-welcome")]
    public async Task<IActionResult> SendWelcome([FromBody] WelcomeRequest request)
    {
        await _email.SendAsync(
            request.Email,
            $"Welcome, {request.Name}",
            $"<h1>Welcome, {request.Name}</h1><p>Your account is ready.</p>"
        );
        return Ok(new { sent = true });
    }
}

Razor Email Templates

Use Razor to build type-safe email templates.

// Services/EmailTemplateService.cs
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
 
public class EmailTemplateService
{
    private readonly IRazorViewEngine _viewEngine;
    private readonly ITempDataProvider _tempDataProvider;
    private readonly IServiceProvider _serviceProvider;
 
    // ... inject dependencies
 
    public async Task<string> RenderAsync<TModel>(string viewName, TModel model)
    {
        // Render a Razor view to string
        // See Microsoft docs for full implementation
    }
}

For simpler cases, use interpolated strings or a template function:

public static class EmailTemplates
{
    public static string Welcome(string name, string loginUrl) => $"""
        <!DOCTYPE html>
        <html>
          <body style="font-family: sans-serif; background: #f6f9fc; padding: 40px 0;">
            <div style="max-width: 480px; margin: 0 auto; background: #fff; padding: 40px; border-radius: 8px;">
              <h1 style="font-size: 24px;">{name}</h1>
              <p style="color: #374151;">Your account is ready.</p>
              <a href="{loginUrl}"
                 style="display:inline-block; background:#f97316; color:#fff; padding:12px 24px; border-radius:6px; text-decoration:none;">
                Go to Dashboard
              </a>
            </div>
          </body>
        </html>
        """;
}

Background Sending with Hosted Services

Use IHostedService or channels for background email processing:

using System.Threading.Channels;
 
public record EmailJob(string To, string Subject, string Body);
 
public class EmailChannel
{
    private readonly Channel<EmailJob> _channel = Channel.CreateBounded<EmailJob>(100);
    public ChannelWriter<EmailJob> Writer => _channel.Writer;
    public ChannelReader<EmailJob> Reader => _channel.Reader;
}
 
public class EmailWorker : BackgroundService
{
    private readonly EmailChannel _channel;
    private readonly IServiceScopeFactory _scopeFactory;
 
    public EmailWorker(EmailChannel channel, IServiceScopeFactory scopeFactory)
    {
        _channel = channel;
        _scopeFactory = scopeFactory;
    }
 
    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        await foreach (var job in _channel.Reader.ReadAllAsync(ct))
        {
            using var scope = _scopeFactory.CreateScope();
            var email = scope.ServiceProvider.GetRequiredService<IEmailService>();
 
            try
            {
                await email.SendAsync(job.To, job.Subject, job.Body);
            }
            catch (Exception ex)
            {
                // Log and continue
            }
        }
    }
}

Register and use:

builder.Services.AddSingleton<EmailChannel>();
builder.Services.AddHostedService<EmailWorker>();
 
// In your endpoint:
app.MapPost("/api/signup", async (SignupRequest req, EmailChannel channel) =>
{
    var user = await CreateUser(req);
    await channel.Writer.WriteAsync(new EmailJob(
        user.Email, $"Welcome, {user.Name}", EmailTemplates.Welcome(user.Name, "/dashboard")
    ));
    return Results.Ok(new { user });
});

Going to Production

1. Verify Your Domain

Add SPF, DKIM, DMARC DNS records. Required for deliverability.

2. Use User Secrets for Development

dotnet user-secrets set "Email:ApiKey" "sq_your_key"

3. Add Polly for Retries

dotnet add package Microsoft.Extensions.Http.Polly
builder.Services.AddHttpClient<IEmailService, SequenzyEmailService>()
    .AddTransientHttpErrorPolicy(p =>
        p.WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt))));

Beyond Transactional

Sequenzy handles transactional sends, marketing campaigns, automated sequences, and subscriber management from one API. Native Stripe integration for SaaS.

Wrapping Up

  1. SmtpClient is obsolete - use API providers
  2. Dependency injection with IEmailService for testability
  3. Razor or raw string templates for HTML emails
  4. Background services with channels for non-blocking sends
  5. Polly retries for production reliability

Pick your provider, register the service, and start sending.