Quickstart
Phase 1 needs only .NET 10 - no tenant, no Azure. Phase 2 connects to a real tenant. Phase 3 (Authentication) adds caller authentication.
Prerequisites
Section titled “Prerequisites”- .NET SDK 10 (download)
- PowerShell 7+ (
pwshon PATH) - hosts Exchange Online in-process - ExchangeOnlineManagement 3.9.x (the documented baseline; 3.5.0 has a manifest bug):
Terminal window pwsh -Command "Install-Module ExchangeOnlineManagement -MinimumVersion 3.9.0 -Scope CurrentUser" git(for cloning + the docs submodule)- (Phase 2+) an Exchange Online tenant where you’re an admin
Phase 1: clone and run offline (about 2 minutes)
Section titled “Phase 1: clone and run offline (about 2 minutes)”Boots the API in metadata-only mode - the service document, the OData CSDL, and the Explorer UI - without any tenant credentials.
git clone <repo-url> && cd exonautgit submodule update --init --depth 1 # the cmdlet docs live in a submoduledotnet build ExchangeApi.slnxdotnet run --project src/ExchangeApi.Web# → listening on http://localhost:5043| What | URL |
|---|---|
| Service document | http://localhost:5043/exchange/v1.0/ |
| OData CSDL (XML) | http://localhost:5043/exchange/v1.0/$metadata |
| OpenAPI 3 document | http://localhost:5043/openapi/v1.json |
| Liveness / readiness | http://localhost:5043/healthz · /readyz |
| Explorer UI | http://localhost:5043/explorer/ |
In Phase 1 the Explorer browses $metadata, but data endpoints error until a tenant is connected -
that’s expected. On a dev laptop (the Development environment) callers don’t need a token (a
permissive dev handler auto-grants scopes and logs a loud per-request warning). The host refuses
to start in Production without real auth config, so that shortcut can’t leak into production.
Phase 2: connect to a real tenant
Section titled “Phase 2: connect to a real tenant”Create an Entra app registration so the API can sign into EXO unattended, grant it
Exchange.ManageAsApp (+ a directory role: Global Reader for read, Exchange Administrator for
write), then store the settings in .NET user-secrets
(never in source control):
dotnet user-secrets --project src/ExchangeApi.Web initdotnet user-secrets --project src/ExchangeApi.Web set "ExchangeOnline:ConnectionMode" "ClientSecret"dotnet user-secrets --project src/ExchangeApi.Web set "ExchangeOnline:Organization" "<tenant>.onmicrosoft.com"dotnet user-secrets --project src/ExchangeApi.Web set "ExchangeOnline:TenantId" "<tenant-guid>"dotnet user-secrets --project src/ExchangeApi.Web set "ExchangeOnline:AppId" "<app-id-guid>"dotnet user-secrets --project src/ExchangeApi.Web set "ExchangeOnline:ClientSecret" "<secret-value>"Restart, wait for Runspace pool warmed (first sign-in ~15s), then:
curl -s "http://localhost:5043/readyz"curl -s "http://localhost:5043/exchange/v1.0/mailboxes?\$top=2&\$select=Id,RecipientTypeDetails" | jq
