How to send an email in .NET

Introduction
Password resets, order confirmations, notifications - sending email is a requirement on most applications, and the first version is usually simpler than people expect. This video sends email from an ASP.NET Core minimal API on .NET 8 using the built-in SmtpClient against Gmail's SMTP server, wrapped behind an interface and configured with the Options pattern.
It also covers the parts that trip people up: why your Gmail password will not work, where the credentials should actually live, and the point at which SmtpClient stops being the right answer.
🎬 Watch the full video here:
The service behind an interface
Email sending goes behind IMailService so the rest of the app depends on an abstraction, not on System.Net.Mail:
public interface IMailService
{
Task SendEmailAsync(SendEmailRequest sendEmailRequest);
}
SendEmailRequest is a small record: recipient, subject, body. The endpoint is one line that takes the request and calls the service. When you later swap SMTP for a transactional email provider, only the implementation changes.
Configuration through the Options pattern
Host, port, sender address, and password are bound to a typed options class rather than read as loose configuration keys:
public class GmailOptions
{
public const string GmailOptionsKey = "GmailOptions";
public string Host { get; set; }
public int Port { get; set; }
public string Email { get; set; }
public string Password { get; set; }
}
builder.Services.Configure<GmailOptions>(
builder.Configuration.GetSection(GmailOptions.GmailOptionsKey));
The service takes IOptions<GmailOptions> and reads .Value. Gmail's SMTP is smtp.gmail.com on port 587 with SSL enabled.
Sending the message
The implementation builds a MailMessage, configures an SmtpClient with the host, port, credentials, and EnableSsl = true, and calls SendMailAsync. The SmtpClient is wrapped in using so the connection is disposed after the send.
using var smtpClient = new SmtpClient
{
Host = _options.Host,
Port = _options.Port,
Credentials = new NetworkCredential(_options.Email, _options.Password),
EnableSsl = true
};
await smtpClient.SendMailAsync(mailMessage);
The Gmail app password
Your normal Google account password will be rejected. Gmail requires an app password for SMTP: turn on 2-Step Verification, then generate a 16-character app password in your Google account security settings and use that as Password. This is the single most common reason "it does not work" for this exact setup.
Where credentials belong
Not in appsettings.json committed to source control. Locally use .NET user secrets (dotnet user-secrets set "GmailOptions:Password" "..."). In the cloud use environment variables or a secret store (Key Vault, AWS Secrets Manager). The Options binding does not change - only where the value comes from.
When to stop using SmtpClient
SmtpClient and a Gmail account are fine for a side project or internal tool with low volume. For anything customer-facing, move to a transactional email provider (SendGrid, Postmark, Amazon SES, Mailgun, Resend). Reasons:
- Deliverability. Providers manage SPF, DKIM, DMARC, and sender reputation. Mail sent straight from an app server frequently lands in spam or is blocked outright.
- Rate limits. Gmail caps sending sharply. A provider scales.
- Observability. Providers give you delivery, bounce, and open tracking.
SmtpClientgives you an exception or silence. - Microsoft's own guidance. The docs explicitly note
SmtpClientis not recommended for new development and suggest a maintained library or a service.
Most providers offer an HTTP API and a NuGet package, so switching is a new IMailService implementation, not a rewrite.
Don't block the request on sending
Sending mail inline makes the HTTP response wait for an external SMTP handshake. For a password reset that is borderline acceptable; for a bulk notification it is not. Push the send onto a background job (Hangfire, a hosted service, a queue) so the request returns immediately and a failed send can be retried.
Common pitfalls
- Using the account password instead of an app password. It will not authenticate.
- Committing credentials. User secrets locally, environment/secret store in production.
- Sending synchronously in the request path. Offload to a background worker.
- Assuming it landed. Without a provider you have no delivery signal - a successful
SendMailAsynconly means the SMTP server accepted the handoff. - Not disposing
SmtpClient. Wrap it inusing.
Key Takeaways
- Put email sending behind
IMailServiceso the transport is swappable. - Bind SMTP settings to typed options; keep the password in a secret store, never in
appsettings.json. - Gmail SMTP needs a generated app password plus 2-Step Verification,
smtp.gmail.com:587, SSL on. SmtpClient+ Gmail is fine for low-volume internal use; move to a transactional provider for anything customer-facing (deliverability, limits, tracking).- Send from a background job, not inline in the request.
Get the Full Source Code
The complete runnable solution - the mail service, the options binding, and the minimal API endpoint - is available to Patreon supporters. If you want to send a real test email instead of rebuilding it from the walkthrough above, you can find it on Patreon.