Blaztrap.Aspire.Hosting.SendGrid
0.1.0
dotnet add package Blaztrap.Aspire.Hosting.SendGrid --version 0.1.0
NuGet\Install-Package Blaztrap.Aspire.Hosting.SendGrid -Version 0.1.0
<PackageReference Include="Blaztrap.Aspire.Hosting.SendGrid" Version="0.1.0" />
<PackageVersion Include="Blaztrap.Aspire.Hosting.SendGrid" Version="0.1.0" />
<PackageReference Include="Blaztrap.Aspire.Hosting.SendGrid" />
paket add Blaztrap.Aspire.Hosting.SendGrid --version 0.1.0
#r "nuget: Blaztrap.Aspire.Hosting.SendGrid, 0.1.0"
#:package Blaztrap.Aspire.Hosting.SendGrid@0.1.0
#addin nuget:?package=Blaztrap.Aspire.Hosting.SendGrid&version=0.1.0
#tool nuget:?package=Blaztrap.Aspire.Hosting.SendGrid&version=0.1.0
Blaztrap.Aspire.Hosting.SendGrid
In-process SendGrid Email API emulator for .NET 10 + Aspire, shipped as a single NuGet package
that plugs straight into any Aspire AppHost via builder.AddSendGrid("sendgrid") — no Projects.X
metadata reference, no separate runner project.
Its headline property: consumers keep the real SendGrid SDK completely unchanged. No code change,
and not even the API key has to change. The emulator speaks the real SendGrid v3 Mail Send REST API,
and because the SDK hard-codes https://api.sendgrid.com, the emulator intercepts the wired
consumer's HTTPS egress with a per-app proxy instead of asking you to repoint the client.
The same assembly carries:
- the emulator server (an ASP.NET Core app the AppHost relaunches via
dotnet <ownDll>), - the AppHost integration (
AddSendGrid,WithSendGrid,WithBlobStorage), - a per-app HTTPS interception proxy + certificate authority (the zero-code-change mechanism),
- a Mailpit-style web dashboard + query REST API for viewing and asserting on captured mail,
- delivery sinks (logging, in-memory store, callback, and optional
.eml→ Blob persistence).
Install
dotnet add package Blaztrap.Aspire.Hosting.SendGrid
Targets net10.0. Requires Aspire.Hosting 13.2+ on the AppHost side. The consumer being wired keeps
its existing SendGrid / SendGrid.Extensions.DependencyInjection reference — this package adds
nothing to it.
Why interception (and not a connection string)
| ACS / connection-string SDKs | SendGrid | |
|---|---|---|
| Endpoint | carried in the connection string → just swap it | hard-coded to api.sendgrid.com in the SDK |
| Reads env/config of its own | — | no (verified against the SDK source) |
| Redirect without code change | trivial | requires network interception + a trusted TLS cert |
So this package ships a certificate authority and an interception proxy. WithSendGrid(consumer)
injects, into only that consumer's process:
HTTPS_PROXY=http://localhost:<proxyPort>— .NET'sHttpClient.DefaultProxyhonours this cross-platform, so every HTTPS request arrives at the emulator as an HTTPCONNECT. Only HTTPS is routed; plain HTTP and loopback (NO_PROXY) stay direct.SSL_CERT_FILE=<emulator CA PEM>— per-process TLS trust for container / Linux consumers.
The proxy man-in-the-middles *.sendgrid.com (presents a leaf certificate signed by the emulator's
CA, terminates TLS, serves /v3/mail/send) and transparently tunnels every other host, so the
consumer's unrelated HTTPS traffic is untouched.
Quick start
In your Aspire AppHost project:
using Blaztrap.Aspire.Hosting.SendGrid;
var builder = DistributedApplication.CreateBuilder(args);
var sendgrid = builder.AddSendGrid("sendgrid"); // emulator: mail API + MITM proxy + dashboard
builder.AddProject<Projects.Worker>("worker")
.WithSendGrid(sendgrid); // intercepts THIS app only — no code change
await builder.Build().RunAsync();
The worker keeps its existing SendGrid registration verbatim — for example the standard DI form:
// unchanged production code
builder.Services.AddSendGrid(o => o.ApiKey = builder.Configuration["SendGrid:ApiKey"]);
// ... ISendGridClient injected, targeting api.sendgrid.com, now captured by the emulator
new SendGridClient(apiKey) and FluentEmail's UseSendGrid(o => o.ApiKey = …) work the same way —
all of them target api.sendgrid.com and are intercepted identically.
Optional .eml persistence
var storage = builder.AddAzureStorage("storage").RunAsEmulator(); // official Azurite Blob emulator
var blobs = storage.AddBlobs("blobs");
var sendgrid = builder.AddSendGrid("sendgrid").WithBlobStorage(blobs);
Without it, emails are still captured, shown in the dashboard and served by the query API — Blob is
just an extra sink that writes each message as an .eml.
Certificate trust (one-time, per platform)
Terminating TLS for api.sendgrid.com requires the consumer to trust the emulator's CA. This is the
only setup step, and it differs by how the consumer runs:
| Consumer runtime | Trust mechanism | Prompt? |
|---|---|---|
Windows / macOS host process (AddProject) |
CA installed into the current user's root store (persisted + reused across runs) | one-time OS consent, then never again |
| Linux container | SSL_CERT_FILE injected per-process by WithSendGrid |
none |
AddSendGrid performs the user-store install at AppHost startup on Windows/macOS. Set
SendGrid:TrustCaInUserStore=false to opt out (e.g. in CI where the CA is pre-trusted, or when you
manage trust yourself). The CA lives under %LOCALAPPDATA%\Blaztrap\SendGridEmulator and can be
removed from the user root store to fully revert.
Dashboard & query API
Both are on by default (SendGrid:EnableDashboard, on-demand toggle) and served from the emulator's
http endpoint:
| Endpoint | Purpose |
|---|---|
GET / · GET /__emulator |
Mailpit-style inbox — subject/from/to, HTML/text/.eml preview |
GET /__emulator/messages |
JSON list of captured emails, newest first |
GET /__emulator/messages/{id} |
full captured email |
GET /__emulator/latest?to=<addr> |
latest message for a recipient — the OTP-scraping test hook |
GET /__emulator/messages/{id}/html · /eml |
rendered HTML body · downloadable .eml |
DELETE /__emulator/messages |
clear the in-memory store |
GET /__admin/health |
liveness probe used by the Aspire health check |
What the SDK sees on the wire
POST /v3/mail/send → 202 Accepted, empty body, X-Message-Id response header — the real
SendGrid success contract. The SDK does not throw on 202 and does not parse the body, and it performs
no API-key format validation, so any key (including your existing one) works unchanged.
Options (SendGrid config section)
| Key | Default | Meaning |
|---|---|---|
EnableLogging |
true |
One-line log per captured email. |
EnableDashboard |
true |
Dashboard + query API + in-memory store. |
EnableBlobStorage |
true |
Persist .eml when Blob storage is wired (no-op otherwise). |
StorageConnectionName |
blobs |
Connection-string name of the Blob sink. |
ContainerName |
emails |
Blob container for .eml files. |
ProxyPort |
injected | MITM proxy port (set by the AppHost from the proxy endpoint). |
TrustCaInUserStore |
true |
Install the CA into the user root store (Windows/macOS host processes). |
MaxStoredEmails |
500 |
In-memory ring-buffer size. |
Testing
Self-host the emulator in-process and drive a real SendGridClient (or your whole SUT) through its
proxy; read the captured message back through the query API to, e.g., scrape an OTP. The repo's
test/Blaztrap.SendGrid.Sample project contains a complete, runnable MSTest + Playwright example
(Interception_Tests, UI/OtpEmail_Tests). On Windows, keep in-process integration tests
non-interactive by setting SendGrid:TrustCaInUserStore=false and trusting the CA at the socket level
in the test (the sample shows the pattern) — the runtime user-store prompt cannot be answered
head-lessly.
How it runs
AddSendGrid registers an ExecutableResource that relaunches this very assembly via
dotnet <ownDll>, so the call site never references a Projects.X type. For NuGet consumers the
package ships runtimeconfig.json / deps.json plus a build/*.targets that copies them next to the
consumer's output.
| 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
- Aspire.Hosting (>= 13.2.1)
- Azure.Storage.Blobs (>= 12.23.0)
- MessagePack (>= 2.5.302)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0 | 45 | 7/16/2026 |