30 | Swapping to the Development environment displays detailed information about the error that occurred.
31 |
32 |
33 | The Development environment shouldn't be enabled for deployed applications.
34 | It can result in displaying sensitive information from exceptions to end users.
35 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development
36 | and restarting the app.
37 |
19 |
20 | An error has occurred. This application may no longer respond until reloaded.
21 |
22 |
23 | An unhandled exception has occurred. See browser dev tools for details.
24 |
25 | Reload
26 | 🗙
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/Frontend/Program.cs:
--------------------------------------------------------------------------------
1 | using MudBlazor.Services;
2 |
3 | var builder = WebApplication.CreateBuilder(args);
4 |
5 | // Add services to the container.
6 | builder.Services.AddRazorPages();
7 | builder.Services.AddServerSideBlazor();
8 | builder.Services.AddMudServices();
9 | builder.Services.AddWebApplicationMonitoring();
10 |
11 | var app = builder.Build();
12 |
13 | // Configure the HTTP request pipeline.
14 | if (!app.Environment.IsDevelopment())
15 | {
16 | app.UseExceptionHandler("/Error");
17 | app.UseHsts();
18 | }
19 |
20 | app.UseHttpsRedirection();
21 | app.UseStaticFiles();
22 | app.UseRouting();
23 | app.MapBlazorHub();
24 | app.MapFallbackToPage("/_Host");
25 |
26 | app.Run();
--------------------------------------------------------------------------------
/Frontend/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:61949",
7 | "sslPort": 44393
8 | }
9 | },
10 | "profiles": {
11 | "IIS Express": {
12 | "commandName": "IISExpress",
13 | "launchBrowser": true,
14 | "environmentVariables": {
15 | "ASPNETCORE_ENVIRONMENT": "Development"
16 | }
17 | },
18 | "Frontend": {
19 | "commandName": "Project",
20 | "launchBrowser": true,
21 | "environmentVariables": {
22 | "ASPNETCORE_ENVIRONMENT": "Development"
23 | },
24 | "applicationUrl": "http://localhost:5002;https://localhost:5003",
25 | "dotnetRunMessages": true
26 | },
27 | "Docker": {
28 | "commandName": "Docker",
29 | "launchBrowser": true,
30 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
31 | "publishAllPorts": true,
32 | "useSSL": true
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/Frontend/Shared/MainLayout.razor:
--------------------------------------------------------------------------------
1 | @inherits LayoutComponentBase
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Sensor Readings
10 |
11 |
12 |
13 |
14 | @Body
15 |
16 |
17 |
18 |
19 | @code {
20 | bool _drawerOpen = true;
21 |
22 | void DrawerToggle()
23 | {
24 | _drawerOpen = !_drawerOpen;
25 | }
26 | }
--------------------------------------------------------------------------------
/Frontend/Shared/NavMenu.razor:
--------------------------------------------------------------------------------
1 |
2 | Home
3 |
--------------------------------------------------------------------------------
/Frontend/_Imports.razor:
--------------------------------------------------------------------------------
1 | @using System.Net.Http
2 | @using Microsoft.AspNetCore.Authorization
3 | @using Microsoft.AspNetCore.Components.Authorization
4 | @using Microsoft.AspNetCore.Components.Forms
5 | @using Microsoft.AspNetCore.Components.Routing
6 | @using Microsoft.AspNetCore.Components.Web
7 | @using Microsoft.AspNetCore.Components.Web.Virtualization
8 | @using Microsoft.JSInterop
9 | @using MudBlazor
10 | @using Frontend
11 | @using Frontend.Shared
12 |
--------------------------------------------------------------------------------
/Frontend/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "DetailedErrors": true,
3 | "Logging": {
4 | "LogLevel": {
5 | "Default": "Information",
6 | "Microsoft": "Warning",
7 | "Microsoft.Hosting.Lifetime": "Information"
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Frontend/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Console": {
3 | "DisableColors": true
4 | },
5 | "Logging": {
6 | "LogLevel": {
7 | "Default": "Information",
8 | "Microsoft": "Warning",
9 | "Microsoft.Hosting.Lifetime": "Information"
10 | }
11 | },
12 | "AllowedHosts": "*",
13 | "ApplicationMapNodeName": "Frontend",
14 | "SERVICE_ENDPOINT": "http://localhost:5000"
15 | }
16 |
--------------------------------------------------------------------------------
/Frontend/wwwroot/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/dotNET-Workers-with-gRPC-messaging-on-Azure-Container-Apps/28d4092dafb1bd2543a73f7fd2f932bc2ddfa1d6/Frontend/wwwroot/favicon.ico
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Microsoft Corporation.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE
--------------------------------------------------------------------------------
/Monitoring/ApplicationMapNodeNameInitializer.cs:
--------------------------------------------------------------------------------
1 | namespace Monitoring
2 | {
3 | using Microsoft.ApplicationInsights.Channel;
4 | using Microsoft.ApplicationInsights.Extensibility;
5 | using Microsoft.Extensions.Configuration;
6 |
7 | public class ApplicationMapNodeNameInitializer : ITelemetryInitializer
8 | {
9 | public ApplicationMapNodeNameInitializer(IConfiguration configuration)
10 | {
11 | Name = configuration["ApplicationMapNodeName"];
12 | }
13 |
14 | public string Name { get; set; }
15 |
16 | public void Initialize(ITelemetry telemetry)
17 | {
18 | telemetry.Context.Cloud.RoleName = Name;
19 | }
20 | }
21 | }
22 |
23 | namespace Microsoft.Extensions.DependencyInjection
24 | {
25 | using Microsoft.ApplicationInsights.Extensibility;
26 | using Monitoring;
27 |
28 | public static class ServiceCollectionExtensions
29 | {
30 | public static void AddWebApplicationMonitoring(this IServiceCollection services)
31 | {
32 | services.AddApplicationInsightsTelemetry();
33 | services.AddSingleton();
34 | }
35 |
36 | public static void AddWorkerApplicationMonitoring(this IServiceCollection services)
37 | {
38 | services.AddApplicationInsightsTelemetryWorkerService();
39 | services.AddSingleton();
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Monitoring/Monitoring.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ASP.NET gRPC backend + Worker + Web Frontend on Azure Container Apps
2 |
3 | This repository contains a simple scenario built to demonstrate how ASP.NET Core 6.0 can be used to build a cloud-native application hosted in Azure Container Apps. The repository consists of the following projects and folders:
4 |
5 | * **Frontend** - A front-end web app written using ASP.NET Core Blazor Server. This web app opens a connection to a gRPC service that streams data to it continuously.
6 | * **Monitoring** - A shared project that makes it simple to configure a .NET project with Application Insights monitoring.
7 | * **SensorService** - A .NET gRPC service that has two features - it receives data from the workers via individual gRPC request/response communication, but also streams data to the frontend Blazor app.
8 | * **SensorWorker** - A .NET Worker Service project that continuously sends data to the `sensorservicegRPC` service inside the cluster. Think of each instance of the `SensorWorker` project as a physical device twin, like a thermometer.
9 | * You'll also see a series of Azure Bicep templates and a GitHub Actions workflow file in the **Azure** and **.github** folders, respectively.
10 |
11 | ## What you'll learn
12 |
13 | This exercise will introduce you to a variety of concepts, with links to supporting documentation throughout the tutorial.
14 |
15 | * [Azure Container Apps](https://docs.microsoft.com/azure/container-apps/overview)
16 | * [GitHub Actions](https://github.com/features/actions)
17 | * [Azure Container Registry](https://docs.microsoft.com/azure/container-registry/)
18 | * [Azure Bicep](https://docs.microsoft.com/azure/azure-resource-manager/bicep/overview?tabs=**bicep**)
19 | * [gRPC](https://grpc.io/) and building [gRPC apps using ASP.NET Core](https://docs.microsoft.com/aspnet/core/grpc/?view=aspnetcore-6.0)
20 |
21 | ## Prerequisites
22 |
23 | You'll need an Azure subscription and a very small set of tools and skills to get started:
24 |
25 | 1. An Azure subscription. Sign up [for free](https://azure.microsoft.com/free/).
26 | 2. A GitHub account, with access to GitHub Actions.
27 | 3. Either the [Azure CLI](https://docs.microsoft.com/cli/azure/install-azure-cli) installed locally, or, access to [GitHub Codespaces](https://github.com/features/codespaces), which would enable you to do develop in your browser.
28 |
29 | ## Topology diagram
30 |
31 | This app is represented by 1 (or more) background worker apps that make repetitive requests via gRPC to a gRPC host container. That host container aggregates the data into a stream after storing it temporarily in Memory Cache. A final container, hosting a frontend Blazor Server application, has a streaming connection to the gRPC service.
32 |
33 | 
34 |
35 |
36 |
37 | As the gRPC service receives data payloads from the individual worker instances, it streams that data constantly to the frontend web app, which displays a graph showing the data being received over time. The app demonstrates how a series of backend services can communicate internally within the Azure Container Apps environment using HTTP2 and gRPC.
38 |
39 | ## Setup
40 |
41 | By the end of this section you'll have a 3-node app running in Azure. This setup process consists of two steps, and should take you around 15 minutes.
42 |
43 | 1. Use the Azure CLI to create an Azure Service Principal, then store that principal's JSON output to a GitHub secret so the GitHub Actions CI/CD process can log into your Azure subscription and deploy the code.
44 | 2. Edit the ` deploy.yml` workflow file and push the changes into a new `deploy` branch, triggering GitHub Actions to build the .NET projects into containers and push those containers into a new Azure Container Apps Environment.
45 |
46 | ## Authenticate to Azure and configure the repository with a secret
47 |
48 | 1. Fork this repository to your own GitHub organization.
49 | 2. Create an Azure Service Principal using the Azure CLI.
50 |
51 | ```bash
52 | $subscriptionId=$(az account show --query id --output tsv)
53 | az ad sp create-for-rbac --sdk-auth --name gRPCAcaSample --role contributor --scopes /subscriptions/$subscriptionId
54 | ```
55 |
56 | 3. Copy the JSON written to the screen to your clipboard.
57 |
58 | ```json
59 | {
60 | "clientId": "",
61 | "clientSecret": "",
62 | "subscriptionId": "",
63 | "tenantId": "",
64 | "activeDirectoryEndpointUrl": "https://login.microsoftonline.com/",
65 | "resourceManagerEndpointUrl": "https://brazilus.management.azure.com",
66 | "activeDirectoryGraphResourceId": "https://graph.windows.net/",
67 | "sqlManagementEndpointUrl": "https://management.core.windows.net:8443/",
68 | "galleryEndpointUrl": "https://gallery.azure.com",
69 | "managementEndpointUrl": "https://management.core.windows.net"
70 | }
71 | ```
72 |
73 | 4. Create a new GitHub secret in your fork of this repository named `AzureSPN`. Paste the JSON returned from the Azure CLI into this new secret. Once you've done this you'll see the secret in your fork of the repository.
74 |
75 | 
76 |
77 | > Note: Never save the JSON to disk, for it will enable anyone who obtains this JSON code to create or edit resources in your Azure subscription.
78 |
79 | ## Deploy the code using GitHub Actions
80 |
81 | The easiest way to deploy the code is to make a commit directly to the `deploy` branch. Do this by navigating to the `deploy.yml` file in your browser and clicking the `Edit` button.
82 |
83 | 
84 |
85 | Provide a custom resource group name for the app, and then commit the change to a new branch named `deploy`.
86 |
87 | 
88 |
89 | Once you click the `Propose changes` button, you'll be in "create a pull request" mode. Don't worry about creating the pull request yet, just click on the `Actions` tab, and you'll see that the deployment CI/CD process has already started.
90 |
91 | 
92 |
93 | When you click into the workflow, you'll see that there are 3 phases the CI/CD will run through:
94 |
95 | 1. provision - the Azure resources will be created that eventually house your app.
96 | 2. build - the various .NET projects are build into containers and published into the Azure Container Registry instance created during provision.
97 | 3. deploy - once `build` completes, the images are in ACR, so the Azure Container Apps are updated to host the newly-published container images.
98 |
99 | 
100 |
101 | After a few minutes, all three steps in the workflow will be completed, and each box in the workflow diagram will reflect success. If anything fails, you can click into the individual process step to see the detailed log output.
102 |
103 | > Note: if you do see any failures or issues, please submit an Issue so we can update the sample. Likewise, if you have ideas that could make it better, feel free to submit a pull request.
104 |
105 | 
106 |
107 | With the projects deployed to Azure, you can now test the app to make sure it works.
108 |
109 | ## Try the app in Azure
110 |
111 | The `deploy` CI/CD process creates a series of resources in your Azure subscription. These are used primarily for hosting the project code, but there's also a few additional resources that aid with monitoring and observing how the app is running in the deployed environment.
112 | | Resource | Resource Type | Purpose |
113 | | ---------------- | ------------------------- | ------------------------------------------------------------ |
114 | | `prefix`grpcai | Application Insights | This provides telemetry about the application's execution, and stores traces, logs, and exception data captured by the Application Insights SDK. |
115 | | frontend | Container App | Hosts the container with the code for the frontend Blazor server application that receives streaming data from the gRPC service. |
116 | | service | Container App | Hosts the container with the code for the gRPC service that both receives requests from the individual Worker services and provides streaming data about the status of the individual workers. |
117 | | worker | Container App | Hosts the container(s) with the code for the Worker Service that sends messages representing sensor data pings (like temperature sensors or light sensors). |
118 | | `prefix`grpcenv | Container App Environment | The Azure Container App Environment, in which all of the container apps running can communicate with one another relatively openly. |
119 | | `prefix`grpcacr | Azure Container Registry | The container registry into which all of my application's microservices are published and stored prior to their being deployed as Azure Container Apps. |
120 | | `prefix`grpclogs | Log Analytics | A Log Analytics account, which provides container logs for all of the container app running in my container app environment. This is where you'll look for most `ILogger` log output using [Kusto](https://docs.microsoft.com/azure/data-explorer/kusto/query/). |
121 |
122 | The resources are shown here in the Azure portal:
123 |
124 | 
125 |
126 | Click on the `frontend` container app to open it up in the Azure portal. In the `Overview` tab you'll see a URL.
127 |
128 | 
129 |
130 | Clicking that URL will open the app's frontend up in the browser. When it opens, you'll see a line chart that fluctuates as it receives data from the streaming gRPC API.
131 |
132 | 
133 |
134 | ## Scale the Worker
135 |
136 | Now, you can scale out the `worker` container app to simulate multiple clients feeding into the gRPC service. To do this, go into the Revision management tool in the Azure portal, and you'll see the revision(s) currently active. You may see 2 revisions even though only one of the revisions is running your code, since the original deployment housed the Azure Container Apps welcome image, and the *second* deployment (the one performed during the `deploy` CI/CD step, since `provision` creates the Azure Container App with the default welcome image).
137 |
138 | 
139 |
140 | Click the Create new revision button, and set the Scale slider to be anything more than 1.
141 |
142 | 
143 |
144 | Once the new revision is provisioned, you should see additional charts appear in the `frontend` web app. Each time a new `worker` starts up, it represents 1 device twin feeding data to the centralized gRPC `service` container app.
145 |
146 | 
147 |
148 | ## Monitoring
149 |
150 | The application is instrumented with Azure Application Insights, and the Azure Container Apps environment has a Log Analytics dependency, so you can easily deep-dive into the logs from the application.
151 |
152 | 
153 |
154 | You can also use the Application Insights Application Map to see a high-level overview of all the nodes and containers in the application, and to see how messages are being transmitted between each container through the Azure Container Apps environment.
155 |
156 | 
157 |
158 | ## Summary
159 |
160 | This sample walks you through the creation of a distributed cloud-native app running in Azure Container Apps, which makes use of gRPC in both request/response and streaming APIs. You've also seen how to monitor and view the application's logs. Take some time and explore Azure Container Apps and what you can do with it as a solid host for your cloud-native .NET apps.
161 |
--------------------------------------------------------------------------------
/SensorApp.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.2.32324.85
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Frontend", "Frontend\Frontend.csproj", "{472EA4A9-0AD2-44BC-A3E8-CFA380EB4D2D}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SensorService", "SensorService\SensorService.csproj", "{1A8526A6-FAD7-46A8-92C9-FD0A8BA31DEF}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SensorWorker", "SensorWorker\SensorWorker.csproj", "{42906AF7-6522-40A4-9B14-AD470DE22BBD}"
11 | EndProject
12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Monitoring", "Monitoring\Monitoring.csproj", "{BBA38D1D-E411-426F-8F59-A71E65F921AD}"
13 | EndProject
14 | Global
15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
16 | Debug|Any CPU = Debug|Any CPU
17 | Release|Any CPU = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
20 | {472EA4A9-0AD2-44BC-A3E8-CFA380EB4D2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {472EA4A9-0AD2-44BC-A3E8-CFA380EB4D2D}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {472EA4A9-0AD2-44BC-A3E8-CFA380EB4D2D}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {472EA4A9-0AD2-44BC-A3E8-CFA380EB4D2D}.Release|Any CPU.Build.0 = Release|Any CPU
24 | {1A8526A6-FAD7-46A8-92C9-FD0A8BA31DEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25 | {1A8526A6-FAD7-46A8-92C9-FD0A8BA31DEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
26 | {1A8526A6-FAD7-46A8-92C9-FD0A8BA31DEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
27 | {1A8526A6-FAD7-46A8-92C9-FD0A8BA31DEF}.Release|Any CPU.Build.0 = Release|Any CPU
28 | {42906AF7-6522-40A4-9B14-AD470DE22BBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
29 | {42906AF7-6522-40A4-9B14-AD470DE22BBD}.Debug|Any CPU.Build.0 = Debug|Any CPU
30 | {42906AF7-6522-40A4-9B14-AD470DE22BBD}.Release|Any CPU.ActiveCfg = Release|Any CPU
31 | {42906AF7-6522-40A4-9B14-AD470DE22BBD}.Release|Any CPU.Build.0 = Release|Any CPU
32 | {BBA38D1D-E411-426F-8F59-A71E65F921AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33 | {BBA38D1D-E411-426F-8F59-A71E65F921AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
34 | {BBA38D1D-E411-426F-8F59-A71E65F921AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
35 | {BBA38D1D-E411-426F-8F59-A71E65F921AD}.Release|Any CPU.Build.0 = Release|Any CPU
36 | EndGlobalSection
37 | GlobalSection(SolutionProperties) = preSolution
38 | HideSolutionNode = FALSE
39 | EndGlobalSection
40 | GlobalSection(ExtensibilityGlobals) = postSolution
41 | SolutionGuid = {F89CC252-4DCF-4E7A-AEA9-F225B2F6A485}
42 | EndGlobalSection
43 | EndGlobal
44 |
--------------------------------------------------------------------------------
/SensorService/Dockerfile:
--------------------------------------------------------------------------------
1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
2 |
3 | FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
4 | WORKDIR /app
5 | EXPOSE 80
6 | EXPOSE 443
7 |
8 | FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
9 | WORKDIR /src
10 | COPY ["SensorService/SensorService.csproj", "SensorService/"]
11 | COPY ["Monitoring/Monitoring.csproj", "Monitoring/"]
12 | RUN dotnet restore "SensorService/SensorService.csproj"
13 | COPY . .
14 | WORKDIR "/src/SensorService"
15 | RUN dotnet build "SensorService.csproj" -c Release -o /app/build
16 |
17 | FROM build AS publish
18 | RUN dotnet publish "SensorService.csproj" -c Release -o /app/publish
19 |
20 | FROM base AS final
21 | WORKDIR /app
22 | COPY --from=publish /app/publish .
23 | ENTRYPOINT ["dotnet", "SensorService.dll"]
--------------------------------------------------------------------------------
/SensorService/Program.cs:
--------------------------------------------------------------------------------
1 | using SensorService.Services;
2 |
3 | var builder = WebApplication.CreateBuilder(args);
4 |
5 | // Add services to the container.
6 | builder.Services.AddGrpc();
7 | builder.Services.AddMemoryCache();
8 | builder.Services.AddWebApplicationMonitoring();
9 |
10 | var app = builder.Build();
11 |
12 | // Configure the HTTP request pipeline.
13 | app.MapGrpcService();
14 | app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
15 |
16 | app.Run();
17 |
--------------------------------------------------------------------------------
/SensorService/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "SensorService": {
4 | "commandName": "Project",
5 | "environmentVariables": {
6 | "ASPNETCORE_ENVIRONMENT": "Development"
7 | },
8 | "applicationUrl": "http://localhost:5000;https://localhost:5001",
9 | "dotnetRunMessages": true
10 | },
11 | "Docker": {
12 | "commandName": "Docker",
13 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
14 | "publishAllPorts": true,
15 | "useSSL": true
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/SensorService/Protos/sensor.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 | import "google/protobuf/empty.proto";
3 |
4 | option csharp_namespace = "Sensors";
5 |
6 | package sensor;
7 |
8 | service SensorTwin {
9 | rpc ReceiveValueFromTwin (ReceiveValueFromTwinRequest) returns (ReceivedValueFromTwinReply);
10 | rpc GetDeviceTwinStream (google.protobuf.Empty) returns (stream ReceivedValueFromTwinReply);
11 | }
12 |
13 | message ReceiveValueFromTwinRequest {
14 | string sensor = 1;
15 | double value = 2;
16 | }
17 |
18 | message ReceivedValueFromTwinReply {
19 | string message = 1;
20 | string sensor = 2;
21 | double value = 3;
22 | }
23 |
--------------------------------------------------------------------------------
/SensorService/SensorService.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | enable
7 | 3d9bc2a5-7a6e-42ae-83f3-39aa1a0282c7
8 | Linux
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/SensorService/Services/SensorTwinService.cs:
--------------------------------------------------------------------------------
1 | using Google.Protobuf.WellKnownTypes;
2 | using Grpc.Core;
3 | using Microsoft.Extensions.Caching.Memory;
4 | using Sensors;
5 |
6 | namespace SensorService.Services
7 | {
8 | public class SensorTwinService : Sensors.SensorTwin.SensorTwinBase
9 | {
10 | const string CACHE_KEY = "READINGS_";
11 | public IMemoryCache MemoryCache { get; set; }
12 |
13 | public SensorTwinService(IMemoryCache memoryCache)
14 | {
15 | MemoryCache = memoryCache;
16 | }
17 |
18 | private void SetRecentReadings(Dictionary> input) => MemoryCache.Set>>(CACHE_KEY, input);
19 |
20 | private Dictionary> GetRecentReadings()
21 | {
22 | var tmp = new Dictionary>();
23 | if (!MemoryCache.TryGetValue>>(CACHE_KEY, out tmp))
24 | {
25 | tmp = new Dictionary>();
26 | SetRecentReadings(tmp);
27 | }
28 |
29 | return tmp;
30 | }
31 |
32 | public override async Task GetDeviceTwinStream(Empty request, IServerStreamWriter responseStream, ServerCallContext context)
33 | {
34 | while (!context.CancellationToken.IsCancellationRequested)
35 | {
36 | var readings = GetRecentReadings();
37 | SetRecentReadings(new Dictionary>());
38 |
39 | foreach (var item in readings)
40 | {
41 | foreach (var value in item.Value)
42 | {
43 | await responseStream.WriteAsync(new ReceivedValueFromTwinReply
44 | {
45 | Message = $"Received {value} from sensor {item.Key}.",
46 | Value = value,
47 | Sensor = item.Key
48 | });
49 | }
50 | }
51 |
52 | await Task.Delay(100);
53 | }
54 | }
55 |
56 | public override Task ReceiveValueFromTwin(ReceiveValueFromTwinRequest request, ServerCallContext context)
57 | {
58 | var readings = GetRecentReadings();
59 |
60 | lock(readings)
61 | {
62 | if (!readings.Any(x => x.Key == request.Sensor))
63 | readings.Add(request.Sensor, new List());
64 |
65 | if (readings[request.Sensor].Count >= 100)
66 | readings[request.Sensor].RemoveAt(0);
67 |
68 | readings[request.Sensor].Add(request.Value);
69 | SetRecentReadings(readings);
70 |
71 | return Task.FromResult(new ReceivedValueFromTwinReply
72 | {
73 | Message = $"Received {request.Value} from {request.Sensor}.",
74 | Sensor = request.Sensor,
75 | Value = request.Value
76 | });
77 | }
78 |
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/SensorService/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/SensorService/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Console": {
3 | "DisableColors": true
4 | },
5 | "Logging": {
6 | "LogLevel": {
7 | "Default": "Information",
8 | "Microsoft.AspNetCore": "Warning"
9 | }
10 | },
11 | "AllowedHosts": "*",
12 | "Kestrel": {
13 | "EndpointDefaults": {
14 | "Protocols": "Http2"
15 | }
16 | },
17 | "ApplicationMapNodeName": "Sensor Service"
18 | }
19 |
--------------------------------------------------------------------------------
/SensorWorker/Dockerfile:
--------------------------------------------------------------------------------
1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
2 |
3 | FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base
4 | WORKDIR /app
5 |
6 | FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
7 | WORKDIR /src
8 | COPY ["SensorWorker/SensorWorker.csproj", "SensorWorker/"]
9 | COPY ["Monitoring/Monitoring.csproj", "Monitoring/"]
10 | RUN dotnet restore "SensorWorker/SensorWorker.csproj"
11 | COPY . .
12 | WORKDIR "/src/SensorWorker"
13 | RUN dotnet build "SensorWorker.csproj" -c Release -o /app/build
14 |
15 | FROM build AS publish
16 | RUN dotnet publish "SensorWorker.csproj" -c Release -o /app/publish
17 |
18 | FROM base AS final
19 | WORKDIR /app
20 | COPY --from=publish /app/publish .
21 | ENTRYPOINT ["dotnet", "SensorWorker.dll"]
--------------------------------------------------------------------------------
/SensorWorker/Program.cs:
--------------------------------------------------------------------------------
1 | using SensorWorker;
2 |
3 | IHost host = Host.CreateDefaultBuilder(args)
4 | .ConfigureServices(services =>
5 | {
6 | services.AddHostedService();
7 | services.AddWorkerApplicationMonitoring();
8 | })
9 | .Build();
10 |
11 | await host.RunAsync();
12 |
--------------------------------------------------------------------------------
/SensorWorker/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "SensorWorker": {
4 | "commandName": "Project",
5 | "environmentVariables": {
6 | "DOTNET_ENVIRONMENT": "Development"
7 | },
8 | "dotnetRunMessages": true
9 | },
10 | "Docker": {
11 | "commandName": "Docker"
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/SensorWorker/SensorWorker.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | enable
7 | dotnet-SensorWorker-87575C4C-7753-4C1D-A0E9-617DA49F171B
8 | Linux
9 |
10 |
11 |
12 |
13 |
14 |
15 | all
16 | runtime; build; native; contentfiles; analyzers; buildtransitive
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | Protos\sensor.proto
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/SensorWorker/Worker.cs:
--------------------------------------------------------------------------------
1 | using Sensors;
2 | using System.Diagnostics;
3 |
4 | namespace SensorWorker
5 | {
6 | public class Worker : BackgroundService
7 | {
8 | private readonly ILogger _logger;
9 | private readonly IConfiguration _configuration;
10 | private readonly SensorTwin.SensorTwinClient _sensorTwinClient;
11 | private Random _random = new Random();
12 | private double _phaseStartTemp;
13 | private double _phaseEndTemp;
14 | private double _currentValue;
15 | private double _phaseDurationSeconds;
16 | private const double MinVal = 50;
17 | private const double MaxVal = 85;
18 | private const int MinPhaseDuration = 5;
19 | private const int MaxPhaseDuration = 30;
20 | private Random _rnd = new Random();
21 | private Stopwatch _totalTime = Stopwatch.StartNew();
22 |
23 | public Worker(ILogger logger, IConfiguration configuration)
24 | {
25 | _logger = logger;
26 | _configuration = configuration;
27 | _sensorTwinClient = new Sensors.SensorTwin.SensorTwinClient(Grpc.Net.Client.GrpcChannel.ForAddress(configuration.GetValue("SERVICE_ENDPOINT")));
28 |
29 | StartNewPhase();
30 | }
31 |
32 | protected override async Task ExecuteAsync(CancellationToken stoppingToken)
33 | {
34 | var randomSensorName = new Random().Next(1000, 9999);
35 |
36 | while (!stoppingToken.IsCancellationRequested)
37 | {
38 | _logger.LogInformation($"Sending sensor reading: {_currentValue}");
39 |
40 | UpdateTemperatureReading();
41 | var result = await _sensorTwinClient.ReceiveValueFromTwinAsync(new ReceiveValueFromTwinRequest
42 | {
43 | Sensor = $"{Environment.MachineName}-{randomSensorName}",
44 | Value = Math.Round(_currentValue, 2)
45 | });
46 |
47 | _logger.LogInformation($"Sensor worker received: {result.Message}.");
48 |
49 | await Task.Delay(100, stoppingToken);
50 | }
51 | }
52 |
53 | public void UpdateTemperatureReading()
54 | {
55 | double startRads;
56 | double endRads;
57 | double offset;
58 | if (_phaseStartTemp >= _phaseEndTemp)
59 | {
60 | // Cooling phase, that means moving from PI/2 to 3*PI/2
61 | startRads = Math.PI / 2;
62 | endRads = 3 * Math.PI / 2;
63 | offset = -1;
64 | }
65 | else
66 | {
67 | // Heading phase, that means moving from 3*PI/2 to 5*PI/2
68 | startRads = 3 * Math.PI / 2;
69 | endRads = 5 * Math.PI / 2;
70 | offset = 1;
71 | }
72 |
73 | var currentSeconds = _totalTime.Elapsed.TotalSeconds;
74 | var currentRads = startRads + (currentSeconds * Math.PI / _phaseDurationSeconds);
75 |
76 | _currentValue = _phaseStartTemp + ((offset + Math.Sin(currentRads)) / 2) * Math.Abs(_phaseStartTemp - _phaseEndTemp);
77 |
78 | if (currentRads >= endRads)
79 | {
80 | StartNewPhase();
81 | }
82 | }
83 |
84 | private void StartNewPhase()
85 | {
86 | _phaseStartTemp = _currentValue;
87 | _phaseEndTemp = MinVal + _rnd.NextDouble() * (MaxVal - MinVal);
88 | _phaseDurationSeconds = _rnd.Next(MinPhaseDuration, MaxPhaseDuration);
89 | _totalTime.Restart();
90 | }
91 | }
92 | }
--------------------------------------------------------------------------------
/SensorWorker/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.Hosting.Lifetime": "Information"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/SensorWorker/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "Console": {
4 | "DisableColors": true
5 | },
6 | "LogLevel": {
7 | "Default": "Information",
8 | "Microsoft.Hosting.Lifetime": "Information"
9 | }
10 | },
11 | "SERVICE_ENDPOINT": "http://localhost:5000",
12 | "ApplicationMapNodeName": "Sensor Worker"
13 | }
14 |
--------------------------------------------------------------------------------
/docs/media/app-front-end.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/dotNET-Workers-with-gRPC-messaging-on-Azure-Container-Apps/28d4092dafb1bd2543a73f7fd2f932bc2ddfa1d6/docs/media/app-front-end.png
--------------------------------------------------------------------------------
/docs/media/appmap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/dotNET-Workers-with-gRPC-messaging-on-Azure-Container-Apps/28d4092dafb1bd2543a73f7fd2f932bc2ddfa1d6/docs/media/appmap.png
--------------------------------------------------------------------------------
/docs/media/create-the-deployment-branch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/dotNET-Workers-with-gRPC-messaging-on-Azure-Container-Apps/28d4092dafb1bd2543a73f7fd2f932bc2ddfa1d6/docs/media/create-the-deployment-branch.png
--------------------------------------------------------------------------------
/docs/media/deployed-to-azure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/dotNET-Workers-with-gRPC-messaging-on-Azure-Container-Apps/28d4092dafb1bd2543a73f7fd2f932bc2ddfa1d6/docs/media/deployed-to-azure.png
--------------------------------------------------------------------------------
/docs/media/deployment-phases.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/dotNET-Workers-with-gRPC-messaging-on-Azure-Container-Apps/28d4092dafb1bd2543a73f7fd2f932bc2ddfa1d6/docs/media/deployment-phases.png
--------------------------------------------------------------------------------
/docs/media/deployment-started.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/dotNET-Workers-with-gRPC-messaging-on-Azure-Container-Apps/28d4092dafb1bd2543a73f7fd2f932bc2ddfa1d6/docs/media/deployment-started.png
--------------------------------------------------------------------------------
/docs/media/deployment-success.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/dotNET-Workers-with-gRPC-messaging-on-Azure-Container-Apps/28d4092dafb1bd2543a73f7fd2f932bc2ddfa1d6/docs/media/deployment-success.png
--------------------------------------------------------------------------------
/docs/media/edit-the-deploy-file.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/dotNET-Workers-with-gRPC-messaging-on-Azure-Container-Apps/28d4092dafb1bd2543a73f7fd2f932bc2ddfa1d6/docs/media/edit-the-deploy-file.png
--------------------------------------------------------------------------------
/docs/media/front-end.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/dotNET-Workers-with-gRPC-messaging-on-Azure-Container-Apps/28d4092dafb1bd2543a73f7fd2f932bc2ddfa1d6/docs/media/front-end.png
--------------------------------------------------------------------------------
/docs/media/logs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/dotNET-Workers-with-gRPC-messaging-on-Azure-Container-Apps/28d4092dafb1bd2543a73f7fd2f932bc2ddfa1d6/docs/media/logs.png
--------------------------------------------------------------------------------
/docs/media/new-revision.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/dotNET-Workers-with-gRPC-messaging-on-Azure-Container-Apps/28d4092dafb1bd2543a73f7fd2f932bc2ddfa1d6/docs/media/new-revision.png
--------------------------------------------------------------------------------
/docs/media/new-sensor-workers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/dotNET-Workers-with-gRPC-messaging-on-Azure-Container-Apps/28d4092dafb1bd2543a73f7fd2f932bc2ddfa1d6/docs/media/new-sensor-workers.png
--------------------------------------------------------------------------------
/docs/media/revision-management.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/dotNET-Workers-with-gRPC-messaging-on-Azure-Container-Apps/28d4092dafb1bd2543a73f7fd2f932bc2ddfa1d6/docs/media/revision-management.png
--------------------------------------------------------------------------------
/docs/media/secrets.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/dotNET-Workers-with-gRPC-messaging-on-Azure-Container-Apps/28d4092dafb1bd2543a73f7fd2f932bc2ddfa1d6/docs/media/secrets.png
--------------------------------------------------------------------------------
/docs/media/toplogy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/dotNET-Workers-with-gRPC-messaging-on-Azure-Container-Apps/28d4092dafb1bd2543a73f7fd2f932bc2ddfa1d6/docs/media/toplogy.png
--------------------------------------------------------------------------------