├── .editorconfig ├── .gitignore ├── EventGridDemo.Api ├── EventGridDemo.Api.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── appsettings.Development.json └── appsettings.json ├── EventGridDemo.AppHost ├── .gitignore ├── EventGridDemo.AppHost.csproj ├── EventGridWebHookResource.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── appsettings.Development.json ├── appsettings.json ├── azure.yaml ├── eventgridwebhook.bicep ├── eventgridwebhookbootstrap.bicep └── infra │ ├── api.tmpl.yaml │ ├── grid │ └── eventgridwebhook.bicep │ ├── main.bicep │ ├── main.parameters.json │ ├── publisher.tmpl.yaml │ └── resources.bicep ├── EventGridDemo.Publisher ├── EventGridDemo.Publisher.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── Worker.cs ├── appsettings.Development.json └── appsettings.json ├── EventGridDemo.ServiceDefaults ├── EventGridDemo.ServiceDefaults.csproj └── Extensions.cs ├── EventGridDemo.sln ├── EventGridWebHookBootstrap ├── EventGridWebHookBootstrap.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── appsettings.Development.json └── appsettings.json └── README.md /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # AZPROVISION001: Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. 4 | dotnet_diagnostic.AZPROVISION001.severity = none 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from `dotnet new gitignore` 5 | 6 | # dotenv files 7 | .env 8 | 9 | # User-specific files 10 | *.rsuser 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Mono auto generated files 20 | mono_crash.* 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | [Ww][Ii][Nn]32/ 30 | [Aa][Rr][Mm]/ 31 | [Aa][Rr][Mm]64/ 32 | bld/ 33 | [Bb]in/ 34 | [Oo]bj/ 35 | [Ll]og/ 36 | [Ll]ogs/ 37 | 38 | # Visual Studio 2015/2017 cache/options directory 39 | .vs/ 40 | # Uncomment if you have tasks that create the project's static files in wwwroot 41 | #wwwroot/ 42 | 43 | # Visual Studio 2017 auto generated files 44 | Generated\ Files/ 45 | 46 | # MSTest test Results 47 | [Tt]est[Rr]esult*/ 48 | [Bb]uild[Ll]og.* 49 | 50 | # NUnit 51 | *.VisualState.xml 52 | TestResult.xml 53 | nunit-*.xml 54 | 55 | # Build Results of an ATL Project 56 | [Dd]ebugPS/ 57 | [Rr]eleasePS/ 58 | dlldata.c 59 | 60 | # Benchmark Results 61 | BenchmarkDotNet.Artifacts/ 62 | 63 | # .NET 64 | project.lock.json 65 | project.fragment.lock.json 66 | artifacts/ 67 | 68 | # Tye 69 | .tye/ 70 | 71 | # ASP.NET Scaffolding 72 | ScaffoldingReadMe.txt 73 | 74 | # StyleCop 75 | StyleCopReport.xml 76 | 77 | # Files built by Visual Studio 78 | *_i.c 79 | *_p.c 80 | *_h.h 81 | *.ilk 82 | *.meta 83 | *.obj 84 | *.iobj 85 | *.pch 86 | *.pdb 87 | *.ipdb 88 | *.pgc 89 | *.pgd 90 | *.rsp 91 | *.sbr 92 | *.tlb 93 | *.tli 94 | *.tlh 95 | *.tmp 96 | *.tmp_proj 97 | *_wpftmp.csproj 98 | *.log 99 | *.tlog 100 | *.vspscc 101 | *.vssscc 102 | .builds 103 | *.pidb 104 | *.svclog 105 | *.scc 106 | 107 | # Chutzpah Test files 108 | _Chutzpah* 109 | 110 | # Visual C++ cache files 111 | ipch/ 112 | *.aps 113 | *.ncb 114 | *.opendb 115 | *.opensdf 116 | *.sdf 117 | *.cachefile 118 | *.VC.db 119 | *.VC.VC.opendb 120 | 121 | # Visual Studio profiler 122 | *.psess 123 | *.vsp 124 | *.vspx 125 | *.sap 126 | 127 | # Visual Studio Trace Files 128 | *.e2e 129 | 130 | # TFS 2012 Local Workspace 131 | $tf/ 132 | 133 | # Guidance Automation Toolkit 134 | *.gpState 135 | 136 | # ReSharper is a .NET coding add-in 137 | _ReSharper*/ 138 | *.[Rr]e[Ss]harper 139 | *.DotSettings.user 140 | 141 | # TeamCity is a build add-in 142 | _TeamCity* 143 | 144 | # DotCover is a Code Coverage Tool 145 | *.dotCover 146 | 147 | # AxoCover is a Code Coverage Tool 148 | .axoCover/* 149 | !.axoCover/settings.json 150 | 151 | # Coverlet is a free, cross platform Code Coverage Tool 152 | coverage*.json 153 | coverage*.xml 154 | coverage*.info 155 | 156 | # Visual Studio code coverage results 157 | *.coverage 158 | *.coveragexml 159 | 160 | # NCrunch 161 | _NCrunch_* 162 | .*crunch*.local.xml 163 | nCrunchTemp_* 164 | 165 | # MightyMoose 166 | *.mm.* 167 | AutoTest.Net/ 168 | 169 | # Web workbench (sass) 170 | .sass-cache/ 171 | 172 | # Installshield output folder 173 | [Ee]xpress/ 174 | 175 | # DocProject is a documentation generator add-in 176 | DocProject/buildhelp/ 177 | DocProject/Help/*.HxT 178 | DocProject/Help/*.HxC 179 | DocProject/Help/*.hhc 180 | DocProject/Help/*.hhk 181 | DocProject/Help/*.hhp 182 | DocProject/Help/Html2 183 | DocProject/Help/html 184 | 185 | # Click-Once directory 186 | publish/ 187 | 188 | # Publish Web Output 189 | *.[Pp]ublish.xml 190 | *.azurePubxml 191 | # Note: Comment the next line if you want to checkin your web deploy settings, 192 | # but database connection strings (with potential passwords) will be unencrypted 193 | *.pubxml 194 | *.publishproj 195 | 196 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 197 | # checkin your Azure Web App publish settings, but sensitive information contained 198 | # in these scripts will be unencrypted 199 | PublishScripts/ 200 | 201 | # NuGet Packages 202 | *.nupkg 203 | # NuGet Symbol Packages 204 | *.snupkg 205 | # The packages folder can be ignored because of Package Restore 206 | **/[Pp]ackages/* 207 | # except build/, which is used as an MSBuild target. 208 | !**/[Pp]ackages/build/ 209 | # Uncomment if necessary however generally it will be regenerated when needed 210 | #!**/[Pp]ackages/repositories.config 211 | # NuGet v3's project.json files produces more ignorable files 212 | *.nuget.props 213 | *.nuget.targets 214 | 215 | # Microsoft Azure Build Output 216 | csx/ 217 | *.build.csdef 218 | 219 | # Microsoft Azure Emulator 220 | ecf/ 221 | rcf/ 222 | 223 | # Windows Store app package directories and files 224 | AppPackages/ 225 | BundleArtifacts/ 226 | Package.StoreAssociation.xml 227 | _pkginfo.txt 228 | *.appx 229 | *.appxbundle 230 | *.appxupload 231 | 232 | # Visual Studio cache files 233 | # files ending in .cache can be ignored 234 | *.[Cc]ache 235 | # but keep track of directories ending in .cache 236 | !?*.[Cc]ache/ 237 | 238 | # Others 239 | ClientBin/ 240 | ~$* 241 | *~ 242 | *.dbmdl 243 | *.dbproj.schemaview 244 | *.jfm 245 | *.pfx 246 | *.publishsettings 247 | orleans.codegen.cs 248 | 249 | # Including strong name files can present a security risk 250 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 251 | #*.snk 252 | 253 | # Since there are multiple workflows, uncomment next line to ignore bower_components 254 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 255 | #bower_components/ 256 | 257 | # RIA/Silverlight projects 258 | Generated_Code/ 259 | 260 | # Backup & report files from converting an old project file 261 | # to a newer Visual Studio version. Backup files are not needed, 262 | # because we have git ;-) 263 | _UpgradeReport_Files/ 264 | Backup*/ 265 | UpgradeLog*.XML 266 | UpgradeLog*.htm 267 | ServiceFabricBackup/ 268 | *.rptproj.bak 269 | 270 | # SQL Server files 271 | *.mdf 272 | *.ldf 273 | *.ndf 274 | 275 | # Business Intelligence projects 276 | *.rdl.data 277 | *.bim.layout 278 | *.bim_*.settings 279 | *.rptproj.rsuser 280 | *- [Bb]ackup.rdl 281 | *- [Bb]ackup ([0-9]).rdl 282 | *- [Bb]ackup ([0-9][0-9]).rdl 283 | 284 | # Microsoft Fakes 285 | FakesAssemblies/ 286 | 287 | # GhostDoc plugin setting file 288 | *.GhostDoc.xml 289 | 290 | # Node.js Tools for Visual Studio 291 | .ntvs_analysis.dat 292 | node_modules/ 293 | 294 | # Visual Studio 6 build log 295 | *.plg 296 | 297 | # Visual Studio 6 workspace options file 298 | *.opt 299 | 300 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 301 | *.vbw 302 | 303 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 304 | *.vbp 305 | 306 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 307 | *.dsw 308 | *.dsp 309 | 310 | # Visual Studio 6 technical files 311 | *.ncb 312 | *.aps 313 | 314 | # Visual Studio LightSwitch build output 315 | **/*.HTMLClient/GeneratedArtifacts 316 | **/*.DesktopClient/GeneratedArtifacts 317 | **/*.DesktopClient/ModelManifest.xml 318 | **/*.Server/GeneratedArtifacts 319 | **/*.Server/ModelManifest.xml 320 | _Pvt_Extensions 321 | 322 | # Paket dependency manager 323 | .paket/paket.exe 324 | paket-files/ 325 | 326 | # FAKE - F# Make 327 | .fake/ 328 | 329 | # CodeRush personal settings 330 | .cr/personal 331 | 332 | # Python Tools for Visual Studio (PTVS) 333 | __pycache__/ 334 | *.pyc 335 | 336 | # Cake - Uncomment if you are using it 337 | # tools/** 338 | # !tools/packages.config 339 | 340 | # Tabs Studio 341 | *.tss 342 | 343 | # Telerik's JustMock configuration file 344 | *.jmconfig 345 | 346 | # BizTalk build output 347 | *.btp.cs 348 | *.btm.cs 349 | *.odx.cs 350 | *.xsd.cs 351 | 352 | # OpenCover UI analysis results 353 | OpenCover/ 354 | 355 | # Azure Stream Analytics local run output 356 | ASALocalRun/ 357 | 358 | # MSBuild Binary and Structured Log 359 | *.binlog 360 | 361 | # NVidia Nsight GPU debugger configuration file 362 | *.nvuser 363 | 364 | # MFractors (Xamarin productivity tool) working folder 365 | .mfractor/ 366 | 367 | # Local History for Visual Studio 368 | .localhistory/ 369 | 370 | # Visual Studio History (VSHistory) files 371 | .vshistory/ 372 | 373 | # BeatPulse healthcheck temp database 374 | healthchecksdb 375 | 376 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 377 | MigrationBackup/ 378 | 379 | # Ionide (cross platform F# VS Code tools) working folder 380 | .ionide/ 381 | 382 | # Fody - auto-generated XML schema 383 | FodyWeavers.xsd 384 | 385 | # VS Code files for those working on multiple tools 386 | .vscode/* 387 | !.vscode/settings.json 388 | !.vscode/tasks.json 389 | !.vscode/launch.json 390 | !.vscode/extensions.json 391 | *.code-workspace 392 | 393 | # Local History for Visual Studio Code 394 | .history/ 395 | 396 | # Windows Installer files from build outputs 397 | *.cab 398 | *.msi 399 | *.msix 400 | *.msm 401 | *.msp 402 | 403 | # JetBrains Rider 404 | *.sln.iml 405 | .idea 406 | 407 | ## 408 | ## Visual studio for Mac 409 | ## 410 | 411 | 412 | # globs 413 | Makefile.in 414 | *.userprefs 415 | *.usertasks 416 | config.make 417 | config.status 418 | aclocal.m4 419 | install-sh 420 | autom4te.cache/ 421 | *.tar.gz 422 | tarballs/ 423 | test-results/ 424 | 425 | # Mac bundle stuff 426 | *.dmg 427 | *.app 428 | 429 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 430 | # General 431 | .DS_Store 432 | .AppleDouble 433 | .LSOverride 434 | 435 | # Icon must end with two \r 436 | Icon 437 | 438 | 439 | # Thumbnails 440 | ._* 441 | 442 | # Files that might appear in the root of a volume 443 | .DocumentRevisions-V100 444 | .fseventsd 445 | .Spotlight-V100 446 | .TemporaryItems 447 | .Trashes 448 | .VolumeIcon.icns 449 | .com.apple.timemachine.donotpresent 450 | 451 | # Directories potentially created on remote AFP share 452 | .AppleDB 453 | .AppleDesktop 454 | Network Trash Folder 455 | Temporary Items 456 | .apdisk 457 | 458 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 459 | # Windows thumbnail cache files 460 | Thumbs.db 461 | ehthumbs.db 462 | ehthumbs_vista.db 463 | 464 | # Dump file 465 | *.stackdump 466 | 467 | # Folder config file 468 | [Dd]esktop.ini 469 | 470 | # Recycle Bin used on file shares 471 | $RECYCLE.BIN/ 472 | 473 | # Windows Installer files 474 | *.cab 475 | *.msi 476 | *.msix 477 | *.msm 478 | *.msp 479 | 480 | # Windows shortcuts 481 | *.lnk 482 | 483 | # Vim temporary swap files 484 | *.swp 485 | -------------------------------------------------------------------------------- /EventGridDemo.Api/EventGridDemo.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | net8.0 9 | enable 10 | enable 11 | 12 | 13 | -------------------------------------------------------------------------------- /EventGridDemo.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | using Microsoft.AspNetCore.HttpLogging; 3 | 4 | var builder = WebApplication.CreateBuilder(args); 5 | 6 | builder.AddServiceDefaults(); 7 | 8 | builder.Services.AddHttpLogging(o => 9 | { 10 | o.LoggingFields = HttpLoggingFields.All; 11 | }); 12 | 13 | var app = builder.Build(); 14 | 15 | app.UseHttpLogging(); 16 | 17 | app.MapDefaultEndpoints(); 18 | 19 | app.MapPost("/hook", (JsonArray gridEvents) => 20 | { 21 | if (gridEvents.Count == 0) 22 | { 23 | return Results.NoContent(); 24 | } 25 | 26 | // Webhook validation 27 | // https://learn.microsoft.com/en-us/azure/event-grid/webhook-event-delivery#validation-details 28 | var firstEvent = gridEvents[0]; 29 | var validationCode = firstEvent?["data"]?["validationCode"]; 30 | 31 | return Results.Ok(new { validationResponse = validationCode }); 32 | }); 33 | 34 | app.Run(); 35 | -------------------------------------------------------------------------------- /EventGridDemo.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:4158", 8 | "sslPort": 44359 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "applicationUrl": "http://localhost:5207", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "https": { 22 | "commandName": "Project", 23 | "dotnetRunMessages": true, 24 | "launchBrowser": true, 25 | "applicationUrl": "https://localhost:7043;http://localhost:5207", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | }, 30 | "IIS Express": { 31 | "commandName": "IISExpress", 32 | "launchBrowser": true, 33 | "environmentVariables": { 34 | "ASPNETCORE_ENVIRONMENT": "Development" 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /EventGridDemo.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Information" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /EventGridDemo.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Information" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /EventGridDemo.AppHost/.gitignore: -------------------------------------------------------------------------------- 1 | .azure 2 | -------------------------------------------------------------------------------- /EventGridDemo.AppHost/EventGridDemo.AppHost.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | true 9 | ba0c27ca-165e-4844-9c73-3802f088a369 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /EventGridDemo.AppHost/EventGridWebHookResource.cs: -------------------------------------------------------------------------------- 1 | using Aspire.Hosting.Azure; 2 | 3 | public class EventGridWebHookResource(string name) : AzureBicepResource(name, templateFile: "eventgridwebhook.bicep"), IResourceWithConnectionString 4 | { 5 | public BicepOutputReference Endpoint => new("endpoint", this); 6 | 7 | public ReferenceExpression ConnectionStringExpression => 8 | ReferenceExpression.Create($"{Endpoint}"); 9 | } 10 | 11 | public static class EventGridExtensions 12 | { 13 | public static IResourceBuilder AddEventGridWebHook(this IDistributedApplicationBuilder builder, string name) 14 | { 15 | builder.AddAzureProvisioning(); 16 | 17 | var resource = new EventGridWebHookResource(name); 18 | return builder.AddResource(resource) 19 | .WithParameter("topicName", name.ToLowerInvariant()) 20 | .WithParameter(AzureBicepResource.KnownParameters.PrincipalId) 21 | .WithParameter(AzureBicepResource.KnownParameters.PrincipalType) 22 | .WithManifestPublishingCallback(resource.WriteToManifest); 23 | } 24 | 25 | public static IResourceBuilder WithWebhookUrl(this IResourceBuilder builder, ReferenceExpression webhookExpression) 26 | { 27 | return builder.WithParameter("webHookEndpoint", () => webhookExpression); 28 | } 29 | 30 | public static IResourceBuilder WithWebhookUrl(this IResourceBuilder builder, ReferenceExpression.ExpressionInterpolatedStringHandler webhookExpression) 31 | { 32 | return builder.WithWebhookUrl(ReferenceExpression.Create(webhookExpression)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /EventGridDemo.AppHost/Program.cs: -------------------------------------------------------------------------------- 1 | var builder = DistributedApplication.CreateBuilder(args); 2 | 3 | var api = builder.AddProject("api") 4 | .WithExternalHttpEndpoints(); 5 | 6 | var eventGrid = builder.AddEventGridWebHook("grid") 7 | .WithWebhookUrl($"{api.GetEndpoint("https")}/hook"); 8 | 9 | builder.AddProject("publisher") 10 | .WithReference(eventGrid); 11 | 12 | builder.Build().Run(); 13 | -------------------------------------------------------------------------------- /EventGridDemo.AppHost/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "https": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "applicationUrl": "https://localhost:17177;http://localhost:15068", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development", 11 | "DOTNET_ENVIRONMENT": "Development", 12 | "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21151", 13 | "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22237" 14 | } 15 | }, 16 | "http": { 17 | "commandName": "Project", 18 | "dotnetRunMessages": true, 19 | "launchBrowser": true, 20 | "applicationUrl": "http://localhost:15068", 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development", 23 | "DOTNET_ENVIRONMENT": "Development", 24 | "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19050", 25 | "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20179" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /EventGridDemo.AppHost/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /EventGridDemo.AppHost/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning", 6 | "Aspire.Hosting.Dcp": "Warning" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /EventGridDemo.AppHost/azure.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json 2 | 3 | name: EventGridDemo.AppHost 4 | services: 5 | app: 6 | language: dotnet 7 | project: .\EventGridDemo.AppHost.csproj 8 | host: containerapp 9 | -------------------------------------------------------------------------------- /EventGridDemo.AppHost/eventgridwebhook.bicep: -------------------------------------------------------------------------------- 1 | param topicName string 2 | param webHookEndpoint string 3 | param principalId string 4 | param principalType string 5 | param location string = resourceGroup().location 6 | 7 | // The topic name must be unique because it's represented by a DNS entry. 8 | // must be between 3-50 characters and contain only values a-z, A-Z, 0-9, and "-". 9 | 10 | resource topic 'Microsoft.EventGrid/topics@2023-12-15-preview' = { 11 | name: toLower(take('${topicName}${uniqueString(resourceGroup().id)}', 50)) 12 | location: location 13 | 14 | resource eventSubscription 'eventSubscriptions' = { 15 | name: 'customSub' 16 | properties: { 17 | destination: { 18 | endpointType: 'WebHook' 19 | properties: { 20 | endpointUrl: webHookEndpoint 21 | } 22 | } 23 | } 24 | } 25 | } 26 | 27 | // https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/integration#eventgrid-data-sender 28 | 29 | resource EventGridRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 30 | name: guid(topic.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'd5a91429-5739-47e2-a06b-3470a27159e7')) 31 | scope: topic 32 | properties: { 33 | principalId: principalId 34 | principalType: principalType 35 | roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'd5a91429-5739-47e2-a06b-3470a27159e7') 36 | } 37 | } 38 | 39 | output endpoint string = topic.properties.endpoint 40 | -------------------------------------------------------------------------------- /EventGridDemo.AppHost/eventgridwebhookbootstrap.bicep: -------------------------------------------------------------------------------- 1 | param location string = resourceGroup().location 2 | param containerAppEnvironmentId string 3 | param webhookPath string 4 | param name string 5 | 6 | resource webhookbootstrapApp 'Microsoft.App/containerApps@2024-03-01' = { 7 | name: name 8 | location: location 9 | properties: { 10 | environmentId: containerAppEnvironmentId 11 | configuration: { 12 | ingress: { 13 | external: true 14 | targetPort: 8080 15 | } 16 | } 17 | template: { 18 | containers: [ 19 | { 20 | name: name 21 | image: 'davidfowl/eventgridwebhookbootstrap' 22 | args: ['--webhookPath', webhookPath] 23 | } 24 | ] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /EventGridDemo.AppHost/infra/api.tmpl.yaml: -------------------------------------------------------------------------------- 1 | location: {{ .Env.AZURE_LOCATION }} 2 | identity: 3 | type: UserAssigned 4 | userAssignedIdentities: 5 | ? "{{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}" 6 | : {} 7 | properties: 8 | environmentId: {{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_ID }} 9 | configuration: 10 | activeRevisionsMode: single 11 | ingress: 12 | external: true 13 | targetPort: {{ targetPortOrDefault 8080 }} 14 | transport: http 15 | allowInsecure: false 16 | registries: 17 | - server: {{ .Env.AZURE_CONTAINER_REGISTRY_ENDPOINT }} 18 | identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }} 19 | template: 20 | containers: 21 | - image: {{ .Image }} 22 | name: api 23 | env: 24 | - name: AZURE_CLIENT_ID 25 | value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }} 26 | - name: ASPNETCORE_FORWARDEDHEADERS_ENABLED 27 | value: "true" 28 | - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES 29 | value: "true" 30 | - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES 31 | value: "true" 32 | - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY 33 | value: in_memory 34 | scale: 35 | minReplicas: 1 36 | tags: 37 | azd-service-name: api 38 | aspire-resource-name: api 39 | -------------------------------------------------------------------------------- /EventGridDemo.AppHost/infra/grid/eventgridwebhook.bicep: -------------------------------------------------------------------------------- 1 | param topicName string 2 | param webHookEndpoint string 3 | param principalId string 4 | param principalType string 5 | param location string = resourceGroup().location 6 | 7 | // The topic name must be unique because it's represented by a DNS entry. 8 | // must be between 3-50 characters and contain only values a-z, A-Z, 0-9, and "-". 9 | 10 | resource topic 'Microsoft.EventGrid/topics@2023-12-15-preview' = { 11 | name: toLower(take('${topicName}${uniqueString(resourceGroup().id)}', 50)) 12 | location: location 13 | 14 | resource eventSubscription 'eventSubscriptions' = { 15 | name: 'customSub' 16 | properties: { 17 | destination: { 18 | endpointType: 'WebHook' 19 | properties: { 20 | endpointUrl: webHookEndpoint 21 | } 22 | } 23 | } 24 | } 25 | } 26 | 27 | // https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/integration#eventgrid-data-sender 28 | 29 | resource EventGridRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 30 | name: guid(topic.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'd5a91429-5739-47e2-a06b-3470a27159e7')) 31 | scope: topic 32 | properties: { 33 | principalId: principalId 34 | principalType: principalType 35 | roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'd5a91429-5739-47e2-a06b-3470a27159e7') 36 | } 37 | } 38 | 39 | output endpoint string = topic.properties.endpoint 40 | -------------------------------------------------------------------------------- /EventGridDemo.AppHost/infra/main.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | @minLength(1) 4 | @maxLength(64) 5 | @description('Name of the environment that can be used as part of naming resource convention, the name of the resource group for your application will use this name, prefixed with rg-') 6 | param environmentName string 7 | 8 | @minLength(1) 9 | @description('The location used for all deployed resources') 10 | param location string 11 | 12 | @description('Id of the user or app to assign application roles') 13 | param principalId string = '' 14 | 15 | 16 | var tags = { 17 | 'azd-env-name': environmentName 18 | } 19 | 20 | resource rg 'Microsoft.Resources/resourceGroups@2022-09-01' = { 21 | name: 'rg-${environmentName}' 22 | location: location 23 | tags: tags 24 | } 25 | 26 | module resources 'resources.bicep' = { 27 | scope: rg 28 | name: 'resources' 29 | params: { 30 | location: location 31 | tags: tags 32 | principalId: principalId 33 | } 34 | } 35 | 36 | module bootstrap '../eventgridwebhookbootstrap.bicep' = { 37 | name: 'webhookbootstrap' 38 | scope: rg 39 | params: { 40 | location: location 41 | containerAppEnvironmentId: resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID 42 | name: 'api' 43 | webhookPath: 'hook' 44 | } 45 | } 46 | 47 | module grid 'grid/eventgridwebhook.bicep' = { 48 | name: 'grid' 49 | scope: rg 50 | params: { 51 | location: location 52 | principalId: resources.outputs.MANAGED_IDENTITY_PRINCIPAL_ID 53 | principalType: 'ServicePrincipal' 54 | topicName: 'grid' 55 | webHookEndpoint: 'https://api.${resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}/hook' 56 | } 57 | } 58 | output MANAGED_IDENTITY_CLIENT_ID string = resources.outputs.MANAGED_IDENTITY_CLIENT_ID 59 | output MANAGED_IDENTITY_NAME string = resources.outputs.MANAGED_IDENTITY_NAME 60 | output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = resources.outputs.AZURE_LOG_ANALYTICS_WORKSPACE_NAME 61 | output AZURE_CONTAINER_REGISTRY_ENDPOINT string = resources.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT 62 | output AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = resources.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID 63 | output AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID 64 | output AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN 65 | 66 | output GRID_ENDPOINT string = grid.outputs.endpoint 67 | -------------------------------------------------------------------------------- /EventGridDemo.AppHost/infra/main.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "principalId": { 6 | "value": "${AZURE_PRINCIPAL_ID}" 7 | }, 8 | "environmentName": { 9 | "value": "${AZURE_ENV_NAME}" 10 | }, 11 | "location": { 12 | "value": "${AZURE_LOCATION}" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /EventGridDemo.AppHost/infra/publisher.tmpl.yaml: -------------------------------------------------------------------------------- 1 | location: {{ .Env.AZURE_LOCATION }} 2 | identity: 3 | type: UserAssigned 4 | userAssignedIdentities: 5 | ? "{{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}" 6 | : {} 7 | properties: 8 | environmentId: {{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_ID }} 9 | configuration: 10 | activeRevisionsMode: single 11 | registries: 12 | - server: {{ .Env.AZURE_CONTAINER_REGISTRY_ENDPOINT }} 13 | identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }} 14 | secrets: 15 | - name: connectionstrings--grid 16 | value: '{{ .Env.GRID_ENDPOINT }}' 17 | template: 18 | containers: 19 | - image: {{ .Image }} 20 | name: publisher 21 | env: 22 | - name: AZURE_CLIENT_ID 23 | value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }} 24 | - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES 25 | value: "true" 26 | - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES 27 | value: "true" 28 | - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY 29 | value: in_memory 30 | - name: ConnectionStrings__grid 31 | secretRef: connectionstrings--grid 32 | scale: 33 | minReplicas: 1 34 | tags: 35 | azd-service-name: publisher 36 | aspire-resource-name: publisher 37 | -------------------------------------------------------------------------------- /EventGridDemo.AppHost/infra/resources.bicep: -------------------------------------------------------------------------------- 1 | @description('The location used for all deployed resources') 2 | param location string = resourceGroup().location 3 | @description('Id of the user or app to assign application roles') 4 | param principalId string = '' 5 | 6 | 7 | @description('Tags that will be applied to all resources') 8 | param tags object = {} 9 | 10 | var resourceToken = uniqueString(resourceGroup().id) 11 | 12 | resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { 13 | name: 'mi-${resourceToken}' 14 | location: location 15 | tags: tags 16 | } 17 | 18 | resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-07-01' = { 19 | name: replace('acr-${resourceToken}', '-', '') 20 | location: location 21 | sku: { 22 | name: 'Basic' 23 | } 24 | properties: { 25 | adminUserEnabled: true 26 | } 27 | tags: tags 28 | } 29 | 30 | resource caeMiRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 31 | name: guid(containerRegistry.id, managedIdentity.id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')) 32 | scope: containerRegistry 33 | properties: { 34 | principalId: managedIdentity.properties.principalId 35 | principalType: 'ServicePrincipal' 36 | roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') 37 | } 38 | } 39 | 40 | resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { 41 | name: 'law-${resourceToken}' 42 | location: location 43 | properties: { 44 | sku: { 45 | name: 'PerGB2018' 46 | } 47 | } 48 | tags: tags 49 | } 50 | 51 | resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-02-02-preview' = { 52 | name: 'cae-${resourceToken}' 53 | location: location 54 | properties: { 55 | workloadProfiles: [{ 56 | workloadProfileType: 'Consumption' 57 | name: 'consumption' 58 | }] 59 | appLogsConfiguration: { 60 | destination: 'log-analytics' 61 | logAnalyticsConfiguration: { 62 | customerId: logAnalyticsWorkspace.properties.customerId 63 | sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey 64 | } 65 | } 66 | } 67 | tags: tags 68 | 69 | resource aspireDashboard 'dotNetComponents' = { 70 | name: 'aspire-dashboard' 71 | properties: { 72 | componentType: 'AspireDashboard' 73 | } 74 | } 75 | } 76 | resource explicitContributorUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 77 | name: guid(containerAppEnvironment.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')) 78 | scope: containerAppEnvironment 79 | properties: { 80 | principalId: principalId 81 | roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') 82 | } 83 | } 84 | 85 | output MANAGED_IDENTITY_CLIENT_ID string = managedIdentity.properties.clientId 86 | output MANAGED_IDENTITY_NAME string = managedIdentity.name 87 | output MANAGED_IDENTITY_PRINCIPAL_ID string = managedIdentity.properties.principalId 88 | output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = logAnalyticsWorkspace.name 89 | output AZURE_LOG_ANALYTICS_WORKSPACE_ID string = logAnalyticsWorkspace.id 90 | output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.properties.loginServer 91 | output AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = managedIdentity.id 92 | output AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = containerAppEnvironment.id 93 | output AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = containerAppEnvironment.properties.defaultDomain 94 | -------------------------------------------------------------------------------- /EventGridDemo.Publisher/EventGridDemo.Publisher.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | dotnet-EventGrid.Publisher-c23e70ad-09f6-4ad5-bb6e-746850cf60a6 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /EventGridDemo.Publisher/Program.cs: -------------------------------------------------------------------------------- 1 | using Azure.Identity; 2 | using Azure.Messaging.EventGrid; 3 | using EventGrid.Publisher; 4 | 5 | var builder = Host.CreateApplicationBuilder(args); 6 | 7 | builder.AddServiceDefaults(); 8 | 9 | builder.Services.AddSingleton(sp => 10 | { 11 | var endpoint = builder.Configuration.GetConnectionString("grid") ?? throw new InvalidOperationException("EventGrid endpoint not found in configuration."); 12 | return new EventGridPublisherClient(new Uri(endpoint), new DefaultAzureCredential()); 13 | }); 14 | 15 | builder.Services.AddHostedService(); 16 | 17 | var host = builder.Build(); 18 | host.Run(); 19 | -------------------------------------------------------------------------------- /EventGridDemo.Publisher/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "EventGrid.Publisher": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "environmentVariables": { 8 | "DOTNET_ENVIRONMENT": "Development" 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EventGridDemo.Publisher/Worker.cs: -------------------------------------------------------------------------------- 1 | using Azure.Messaging.EventGrid; 2 | 3 | namespace EventGrid.Publisher; 4 | 5 | public class Worker(EventGridPublisherClient client, ILogger logger) : BackgroundService 6 | { 7 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 8 | { 9 | while (!stoppingToken.IsCancellationRequested) 10 | { 11 | var payload = new BinaryData(new DateEvent(DateTimeOffset.UtcNow)); 12 | 13 | logger.LogInformation("Sending event at {time}", DateTimeOffset.UtcNow); 14 | 15 | // Send the events 16 | await client.SendEventsAsync([ 17 | new EventGridEvent("ExampleEventSubject", "Example.EventType", "1.0", payload) 18 | ], 19 | stoppingToken); 20 | 21 | await Task.Delay(5000, stoppingToken); 22 | } 23 | } 24 | 25 | record DateEvent(DateTimeOffset Time); 26 | } 27 | 28 | -------------------------------------------------------------------------------- /EventGridDemo.Publisher/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.Hosting.Lifetime": "Information" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /EventGridDemo.Publisher/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.Hosting.Lifetime": "Information" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /EventGridDemo.ServiceDefaults/EventGridDemo.ServiceDefaults.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /EventGridDemo.ServiceDefaults/Extensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Diagnostics.HealthChecks; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Diagnostics.HealthChecks; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.Extensions.ServiceDiscovery; 7 | using OpenTelemetry; 8 | using OpenTelemetry.Metrics; 9 | using OpenTelemetry.Trace; 10 | 11 | namespace Microsoft.Extensions.Hosting; 12 | 13 | public static class Extensions 14 | { 15 | public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) 16 | { 17 | builder.ConfigureOpenTelemetry(); 18 | 19 | builder.AddDefaultHealthChecks(); 20 | 21 | builder.Services.AddServiceDiscovery(); 22 | 23 | builder.Services.ConfigureHttpClientDefaults(http => 24 | { 25 | // Turn on resilience by default 26 | http.AddStandardResilienceHandler(); 27 | 28 | // Turn on service discovery by default 29 | http.AddServiceDiscovery(); 30 | }); 31 | 32 | // Uncomment the following to restrict the allowed schemes for service discovery. 33 | // builder.Services.Configure(options => 34 | // { 35 | // options.AllowedSchemes = ["https"]; 36 | // }); 37 | 38 | return builder; 39 | } 40 | 41 | public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) 42 | { 43 | builder.Logging.AddOpenTelemetry(logging => 44 | { 45 | logging.IncludeFormattedMessage = true; 46 | logging.IncludeScopes = true; 47 | }); 48 | 49 | builder.Services.AddOpenTelemetry() 50 | .WithMetrics(metrics => 51 | { 52 | metrics.AddAspNetCoreInstrumentation() 53 | .AddHttpClientInstrumentation() 54 | .AddRuntimeInstrumentation(); 55 | }) 56 | .WithTracing(tracing => 57 | { 58 | tracing.AddAspNetCoreInstrumentation() 59 | // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) 60 | //.AddGrpcClientInstrumentation() 61 | .AddHttpClientInstrumentation(); 62 | }); 63 | 64 | builder.AddOpenTelemetryExporters(); 65 | 66 | return builder; 67 | } 68 | 69 | private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) 70 | { 71 | var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); 72 | 73 | if (useOtlpExporter) 74 | { 75 | builder.Services.AddOpenTelemetry().UseOtlpExporter(); 76 | } 77 | 78 | // Uncomment the following lines to enable the Prometheus exporter (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) 79 | // builder.Services.AddOpenTelemetry() 80 | // .WithMetrics(metrics => metrics.AddPrometheusExporter()); 81 | 82 | // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) 83 | //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) 84 | //{ 85 | // builder.Services.AddOpenTelemetry() 86 | // .UseAzureMonitor(); 87 | //} 88 | 89 | return builder; 90 | } 91 | 92 | public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) 93 | { 94 | builder.Services.AddHealthChecks() 95 | // Add a default liveness check to ensure app is responsive 96 | .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); 97 | 98 | return builder; 99 | } 100 | 101 | public static WebApplication MapDefaultEndpoints(this WebApplication app) 102 | { 103 | // Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) 104 | // app.MapPrometheusScrapingEndpoint(); 105 | 106 | // Adding health checks endpoints to applications in non-development environments has security implications. 107 | // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. 108 | if (app.Environment.IsDevelopment()) 109 | { 110 | // All health checks must pass for app to be considered ready to accept traffic after starting 111 | app.MapHealthChecks("/health"); 112 | 113 | // Only health checks tagged with the "live" tag must pass for app to be considered alive 114 | app.MapHealthChecks("/alive", new HealthCheckOptions 115 | { 116 | Predicate = r => r.Tags.Contains("live") 117 | }); 118 | } 119 | 120 | return app; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /EventGridDemo.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.10.34818.151 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventGridDemo.AppHost", "EventGridDemo.AppHost\EventGridDemo.AppHost.csproj", "{B6C4A62E-BA95-4731-B050-E3744004325C}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventGridDemo.Api", "EventGridDemo.Api\EventGridDemo.Api.csproj", "{AD754E86-1ABC-4F4E-AFCA-6CF764D42A87}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventGridDemo.Publisher", "EventGridDemo.Publisher\EventGridDemo.Publisher.csproj", "{CD9C8044-B76C-438A-8A56-A6B4B81F101C}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventGridDemo.ServiceDefaults", "EventGridDemo.ServiceDefaults\EventGridDemo.ServiceDefaults.csproj", "{28FEE277-3FEE-44B4-8569-813ECB28DE9C}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventGridWebHookBootstrap", "EventGridWebHookBootstrap\EventGridWebHookBootstrap.csproj", "{B8E03849-7A89-4EBC-864E-9A485329732A}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {B6C4A62E-BA95-4731-B050-E3744004325C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {B6C4A62E-BA95-4731-B050-E3744004325C}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {B6C4A62E-BA95-4731-B050-E3744004325C}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {B6C4A62E-BA95-4731-B050-E3744004325C}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {AD754E86-1ABC-4F4E-AFCA-6CF764D42A87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {AD754E86-1ABC-4F4E-AFCA-6CF764D42A87}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {AD754E86-1ABC-4F4E-AFCA-6CF764D42A87}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {AD754E86-1ABC-4F4E-AFCA-6CF764D42A87}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {CD9C8044-B76C-438A-8A56-A6B4B81F101C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {CD9C8044-B76C-438A-8A56-A6B4B81F101C}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {CD9C8044-B76C-438A-8A56-A6B4B81F101C}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {CD9C8044-B76C-438A-8A56-A6B4B81F101C}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {28FEE277-3FEE-44B4-8569-813ECB28DE9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {28FEE277-3FEE-44B4-8569-813ECB28DE9C}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {28FEE277-3FEE-44B4-8569-813ECB28DE9C}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {28FEE277-3FEE-44B4-8569-813ECB28DE9C}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {B8E03849-7A89-4EBC-864E-9A485329732A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {B8E03849-7A89-4EBC-864E-9A485329732A}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {B8E03849-7A89-4EBC-864E-9A485329732A}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {B8E03849-7A89-4EBC-864E-9A485329732A}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {2FD4B962-ACAA-4EAB-AA44-B324DE43A6AA} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /EventGridWebHookBootstrap/EventGridWebHookBootstrap.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /EventGridWebHookBootstrap/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | using Microsoft.AspNetCore.HttpLogging; 3 | 4 | var builder = WebApplication.CreateBuilder(args); 5 | 6 | var webhookPath = builder.Configuration["WebhookPath"]; 7 | 8 | if (string.IsNullOrEmpty(webhookPath)) 9 | { 10 | throw new InvalidOperationException("WebhookPath is required"); 11 | } 12 | 13 | builder.Services.AddHttpLogging(o => 14 | { 15 | o.LoggingFields = HttpLoggingFields.All; 16 | }); 17 | 18 | var app = builder.Build(); 19 | 20 | app.Logger.LogInformation("Webhook path: {webhookPath}", webhookPath); 21 | 22 | app.UseHttpLogging(); 23 | 24 | app.MapPost(webhookPath, (JsonArray gridEvents) => 25 | { 26 | if (gridEvents.Count == 0) 27 | { 28 | return Results.NoContent(); 29 | } 30 | 31 | // Webhook validation 32 | // https://learn.microsoft.com/en-us/azure/event-grid/webhook-event-delivery#validation-details 33 | var firstEvent = gridEvents[0]; 34 | var validationCode = firstEvent?["data"]?["validationCode"]; 35 | 36 | return Results.Ok(new { validationResponse = validationCode }); 37 | }); 38 | 39 | app.Run(); 40 | -------------------------------------------------------------------------------- /EventGridWebHookBootstrap/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:6471", 8 | "sslPort": 44382 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "applicationUrl": "http://localhost:5010", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "https": { 22 | "commandName": "Project", 23 | "dotnetRunMessages": true, 24 | "launchBrowser": true, 25 | "applicationUrl": "https://localhost:7094;http://localhost:5010", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | }, 30 | "IIS Express": { 31 | "commandName": "IISExpress", 32 | "launchBrowser": true, 33 | "environmentVariables": { 34 | "ASPNETCORE_ENVIRONMENT": "Development" 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /EventGridWebHookBootstrap/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /EventGridWebHookBootstrap/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## EventGrid Webhooks using .NET Aspire 2 | 3 | The code in this repository shows publish and subscribe to events using event grid in .NET Aspire. The code is made up of 2 applications, [EventGridDemo.Publisher](/EventGridDemo.Publisher) and [EventGridDemo.Api](/EventGridDemo.Api). The publisher application publishes events to the event grid topic, and the API application listens for events from the event grid topic. 4 | 5 | 6 | The `/hook` endpoint is used for validating the webhook. When Event Grid creates a new subscription, it sends a validation event to the webhook. The validation event contains a validationCode property. The application reads the validationCode from the first event and returns it as a response. 7 | 8 | The [AppHost](/EventGridDemo.AppHost/) projects creates the eventgrid topic, web hook subscription, sets up the role assignment and orchestrates the deployment of the publisher and api. 9 | 10 | ### Deployment 11 | 12 | Event grid subscriptions require the endpoint to be live. In order to get the application deployed, there must be a 2 step deployment process. This application has uses a [bootstrapper container](https://hub.docker.com/r/davidfowl/eventgridwebhookbootstrap) and 13 | [a bootstrap container app](/EventGridDemo.AppHost/eventgridwebhookbootstrap.bicep) to deploy the application. The bootstrap container is configured in [main.bicep](/EventGridDemo.AppHost/infra/main.bicep#36), which is auto-generated by using `azd infra synth`. 14 | 15 | See https://github.com/Azure/azure-dev/issues/3931 for more details. 16 | 17 | ### Known Issues 18 | 19 | 1. **Local development** - The application is not able to receive events from the Event Grid service when running locally. The application needs to be deployed to a public endpoint to receive events from the Event Grid service. Future efforts will work around this issue by setting up dev tunnels or using ngrok to expose the local endpoint to the public. 20 | --------------------------------------------------------------------------------