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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Blaztrap.Aspire.Hosting.SendGrid" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Blaztrap.Aspire.Hosting.SendGrid" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Blaztrap.Aspire.Hosting.SendGrid" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Blaztrap.Aspire.Hosting.SendGrid --version 0.1.0
                    
#r "nuget: Blaztrap.Aspire.Hosting.SendGrid, 0.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Blaztrap.Aspire.Hosting.SendGrid@0.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Blaztrap.Aspire.Hosting.SendGrid&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Blaztrap.Aspire.Hosting.SendGrid&version=0.1.0
                    
Install as a Cake Tool

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's HttpClient.DefaultProxy honours this cross-platform, so every HTTPS request arrives at the emulator as an HTTP CONNECT. 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/send202 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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