Mailbeam
ASP.NET Core + C#Intermediate20 minutesUpdated January 2025

Email Verification in .NET

This tutorial integrates Mailbeam into an ASP.NET Core application using idiomatic .NET patterns: a typed HttpClient, the options pattern for configuration, dependency injection, and a reusable validation attribute. It works the same in minimal APIs and MVC controllers.

What you'll build

  • An IEmailVerifier service backed by a typed HttpClient
  • A [VerifiedEmail] validation attribute for model binding
  • A minimal API signup endpoint that rejects undeliverable addresses

Prerequisites

  • .NET 8 SDK or later
  • A Mailbeam API key (sign up free)
  • Basic familiarity with dependency injection in ASP.NET Core

Step 1 — Configure the key

Store the key in user secrets locally and in environment variables / Key Vault in production:

dotnet user-secrets init
dotnet user-secrets set "Mailbeam:ApiKey" "mb_live_xxxxxxxxxxxxxxxxxxxx"

Bind it with the options pattern:

// Options/MailbeamOptions.cs
public sealed class MailbeamOptions
{
    public string ApiKey { get; set; } = "";
    public int MinScore { get; set; } = 60;
}

Step 2 — Register a typed HttpClient

// Program.cs
builder.Services.Configure<MailbeamOptions>(
    builder.Configuration.GetSection("Mailbeam"));

builder.Services.AddHttpClient<IEmailVerifier, MailbeamVerifier>((sp, client) =>
{
    var opts = sp.GetRequiredService<IOptions<MailbeamOptions>>().Value;
    client.BaseAddress = new Uri("https://api.mailbeam.dev/");
    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue("Bearer", opts.ApiKey);
    client.Timeout = TimeSpan.FromSeconds(5);
});

Step 3 — Build the verification service

// Services/IEmailVerifier.cs
public interface IEmailVerifier
{
    Task<bool> IsAcceptableAsync(string email, CancellationToken ct = default);
}

// Services/MailbeamVerifier.cs
public sealed class MailbeamVerifier : IEmailVerifier
{
    private readonly HttpClient _http;
    private readonly MailbeamOptions _opts;
    private readonly ILogger<MailbeamVerifier> _log;

    public MailbeamVerifier(
        HttpClient http,
        IOptions<MailbeamOptions> opts,
        ILogger<MailbeamVerifier> log)
    {
        _http = http;
        _opts = opts.Value;
        _log = log;
    }

    private sealed record VerifyResponse(bool Valid, int Score, string? Reason);

    public async Task<bool> IsAcceptableAsync(string email, CancellationToken ct = default)
    {
        try
        {
            var resp = await _http.PostAsJsonAsync(
                "v1/verify", new { email = email.Trim().ToLowerInvariant() }, ct);
            resp.EnsureSuccessStatusCode();

            var result = await resp.Content.ReadFromJsonAsync<VerifyResponse>(ct);
            return result is not null
                && result.Valid
                && result.Score >= _opts.MinScore;
        }
        catch (Exception ex)
        {
            // Fail open: an API error shouldn't block signups
            _log.LogError(ex, "Mailbeam verification failed for {Email}", email);
            return true;
        }
    }
}

Step 4 — Add a validation attribute

For MVC and model binding, a custom attribute keeps validation declarative:

// Validation/VerifiedEmailAttribute.cs
public sealed class VerifiedEmailAttribute : ValidationAttribute
{
    protected override ValidationResult? IsValid(
        object? value, ValidationContext context)
    {
        if (value is not string email || string.IsNullOrWhiteSpace(email))
            return ValidationResult.Success; // let [Required] handle empties

        var verifier = context.GetRequiredService<IEmailVerifier>();
        // Attributes are sync; block briefly on the async call
        var ok = verifier.IsAcceptableAsync(email).GetAwaiter().GetResult();

        return ok
            ? ValidationResult.Success
            : new ValidationResult("Please provide a valid, deliverable email address.");
    }
}

Step 5 — Verify in a minimal API endpoint

// Program.cs
app.MapPost("/api/signup", async (
    SignupRequest req,
    IEmailVerifier verifier,
    CancellationToken ct) =>
{
    if (!await verifier.IsAcceptableAsync(req.Email, ct))
        return Results.ValidationProblem(new Dictionary<string, string[]>
        {
            ["email"] = ["Please provide a valid, deliverable email address."]
        });

    var user = await CreateUserAsync(req.Email, req.Password, ct);
    return Results.Created($"/users/{user.Id}", user);
});

public record SignupRequest(string Email, string Password);

Step 6 — Add caching

Use IMemoryCache (or a distributed cache) to avoid repeat lookups:

public async Task<bool> IsAcceptableAsync(string email, CancellationToken ct = default)
{
    var key = $"mb:{email.Trim().ToLowerInvariant()}";
    if (_cache.TryGetValue<bool>(key, out var cached))
        return cached;

    // ... perform the verify call as in Step 3 ...
    var ok = result is not null && result.Valid && result.Score >= _opts.MinScore;
    _cache.Set(key, ok, TimeSpan.FromHours(24));
    return ok;
}

Testing

Mock the typed client or point tests at Mailbeam's test domains, which are deterministic and don't consume quota:

[Fact]
public async Task Disposable_Email_Is_Rejected()
{
    var verifier = CreateVerifier(); // wired to test config
    var ok = await verifier.IsAcceptableAsync("temp@disposable.mailbeam-test.dev");
    Assert.False(ok);
}

[Fact]
public async Task Valid_Email_Is_Accepted()
{
    var verifier = CreateVerifier();
    var ok = await verifier.IsAcceptableAsync("user@valid.mailbeam-test.dev");
    Assert.True(ok);
}

Best practices

PracticeWhy
Typed HttpClient via AddHttpClientPooling, resilience, easy mocking
Options pattern for the keyClean config binding, no magic strings
Fail open on exceptionsAPI outages don't break signups
Set a 5s TimeoutDon't hang the request on a slow probe
Cache with IMemoryCacheCuts duplicate calls on retries

Production checklist

  • API key in user secrets / Key Vault, never in source
  • HttpClient timeout configured
  • Logging on verification failures
  • Distributed cache for multi-instance deployments
  • Test domains used in unit tests

Next steps