Skip to content

Commit ca435d0

Browse files
committed
Service Invocation
1 parent 9bb8041 commit ca435d0

1 file changed

Lines changed: 334 additions & 4 deletions

File tree

content/courses/dapr-aspire/service-invocation.md

Lines changed: 334 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,337 @@ tags: ['.NET Aspire', 'DAPR']
66
weight: 2
77
---
88

9-
- [ ] Overview
10-
- [ ] Service Invocation with Aspire
11-
- [ ] Service Invocation with DAPR
12-
- [ ] Deployment
9+
This article shows how to use `Service Invocation` with `DAPR`'s building block, combined with `.NET Aspire` for orchestration.
10+
11+
You'll learn how services can communicate reliably in a distributed environment, with `Aspire` managing the setup and `DAPR` providing service discovery and resilient invocation patterns.
12+
13+
## Resources
14+
15+
### 👩‍💻 Source Code
16+
17+
If you are stuck, you can refer the final source code, available at [GitHub Repository](https://github.com/NetRecipes/service-invocation)
18+
19+
## What is Service Invocation, and Why It Matters?
20+
21+
**Service invocation** is the ability of one service to call another service's API in a distributed system.
22+
23+
In microservices architectures, services need to communicate with each other — an order service might call a pricing service, or a frontend might call multiple backend APIs.
24+
25+
Without proper service invocation patterns, you face challenges like hardcoded URLs, lack of service discovery, no retry logic, and difficult debugging across service boundaries.
26+
27+
For example, think of an e‑commerce system where `ServiceA` needs to calculate discounted prices by calling `ServiceB`'s discount calculation endpoint.
28+
Without `DAPR`, you'd need to:
29+
30+
- Manually manage service URLs and ports
31+
- Implement your own retry and timeout logic
32+
- Handle service discovery yourself
33+
- Deal with networking complexities
34+
35+
`DAPR` solves this by providing a simple, consistent API for service-to-service communication, while `Aspire` orchestrates the environment so services can discover and call each other seamlessly.
36+
37+
You get built-in service discovery, automatic retries, distributed tracing, and the flexibility to switch between different invocation methods — all without changing your service code.
38+
39+
> 💡 **Note:** `DAPR`'s service invocation isn't just about making HTTP calls — it provides resilience patterns, observability, and security features that would otherwise require significant custom implementation.
40+
41+
## Hands‑On Setup
42+
43+
We'll scaffold a new `.NET Aspire` solution and add two Web API services that communicate with each other.
44+
45+
Each command below is shown individually with its purpose explained.
46+
47+
### 1. Create the Aspire host project
48+
49+
This sets up the orchestration project named `ServiceInvocation`.
50+
51+
```bash
52+
dotnet new aspire --name ServiceInvocation --no-https --output .
53+
```
54+
55+
### 2. Create the 2 Web API services
56+
57+
Generates 2 Web API projects called `ServiceA` & `ServiceB` using controllers.
58+
59+
And add both services to the solution
60+
61+
```bash
62+
dotnet new webapi --name ServiceA --no-https --use-controllers
63+
dotnet new webapi --name ServiceB --no-https --use-controllers
64+
dotnet sln add .\ServiceA\ .\ServiceB\
65+
```
66+
67+
### 3. Migrate to the new `.slnx` format (Optional)
68+
69+
Converts the solution to the modern format used by Aspire.
70+
And cleans up the legacy solution file, leaving only `ServiceInvocation.slnx`.
71+
72+
```bash
73+
dotnet sln migrate
74+
rm ServiceInvocation.sln
75+
```
76+
77+
> 💡 **Note:** The newer `.slnx` solution format is a general .NET enhancement. It's cleaner and more minimal than the traditional `.sln`, reducing boilerplate and making solutions easier to manage in modern .NET projects.
78+
79+
Now, open `ServiceInvocation.slnx` with `Visual Studio` or `Rider`, or simply open the directory with `VS Code`.
80+
81+
Alternatively, you can skip these steps and clone the final companion repository: [NetRecipes/service-invocation](https://github.com/NetRecipes/service-invocation).
82+
83+
## NuGet Packages
84+
85+
Depending on your IDE, install the following NuGet packages in the specified projects:
86+
87+
### ServiceA and ServiceB
88+
89+
In both `ServiceA` and `ServiceB` projects, install the following NuGet packages to enable `DAPR` integration, API documentation, and UI enhancements.
90+
91+
(Note: `Microsoft.AspNetCore.OpenApi` is usually included by default in the Web API template, so you may not need to install it separately.)
92+
93+
| Package ID | Purpose |
94+
|------------|---------|
95+
| [Dapr.AspNetCore](https://www.nuget.org/packages/Dapr.AspNetCore) | Adds DAPR integration to ASP.NET Core, including service invocation, state management, pub/sub, and bindings. |
96+
| [Swashbuckle.AspNetCore.SwaggerUI](https://www.nuget.org/packages/Swashbuckle.AspNetCore.SwaggerUI) | Provides Swagger UI for interactive API documentation and testing. |
97+
| [AspNetCore.SwaggerUI.Themes](https://www.nuget.org/packages/AspNetCore.SwaggerUI.Themes) | Adds modern, customizable themes to Swagger UI for better developer experience. |
98+
99+
```bash
100+
dotnet add ServiceA package Dapr.AspNetCore
101+
dotnet add ServiceA package Swashbuckle.AspNetCore.SwaggerUI
102+
dotnet add ServiceA package AspNetCore.SwaggerUI.Themes
103+
```
104+
105+
### ServiceInvocation.AppHost
106+
107+
In the `ServiceInvocation.AppHost` project, install the following NuGet package to enable integration with `DAPR` sidecars.
108+
109+
| Package ID | Purpose |
110+
|------------|---------|
111+
| [CommunityToolkit.Aspire.Hosting.Dapr](https://www.nuget.org/packages/CommunityToolkit.Aspire.Hosting.Dapr) | Integrates DAPR sidecars into an Aspire application, enabling service orchestration with DAPR building blocks. |
112+
113+
```bash
114+
dotnet add ServiceInvocation.AppHost package CommunityToolkit.Aspire.Hosting.Dapr
115+
```
116+
117+
## Code Walkthrough
118+
119+
With the setup complete, let's implement service invocation between `ServiceA` and `ServiceB`.
120+
121+
### 1. Program.cs – Add DAPR support (Both Services)
122+
123+
In both `ServiceA/Program.cs` and `ServiceB/Program.cs`, register DAPR services and Swagger UI:
124+
125+
```csharp
126+
using AspNetCore.Swagger.Themes;
127+
128+
var builder = WebApplication.CreateBuilder(args);
129+
130+
builder.AddServiceDefaults(); // Aspire hosting helpers
131+
132+
builder.Services.AddDaprClient(); // Enables DAPR integration
133+
builder.Services.AddControllers().AddDapr(); // Adds DAPR support to controllers
134+
135+
builder.Services.AddOpenApi();
136+
137+
var app = builder.Build();
138+
139+
if (app.Environment.IsDevelopment())
140+
{
141+
app.MapOpenApi();
142+
app.UseSwaggerUI(
143+
ModernStyle.Futuristic,
144+
options => options.SwaggerEndpoint("/openapi/v1.json", "ServiceA v1")); // Change to ServiceB for ServiceB
145+
}
146+
147+
app.UseAuthorization();
148+
149+
app.UseCloudEvents();
150+
app.MapControllers();
151+
app.MapSubscribeHandler();
152+
153+
app.Run();
154+
```
155+
156+
Also create a Common class library project, that can be refered by both `ServiceA` and `ServiceB` to contain a commaon `record` / `POCO`
157+
158+
```cs
159+
public record Order(string Product, int Quantity, decimal PricePerUnit);
160+
```
161+
162+
> It's fine to skip the Common project and just have the record in both the projects as well, for simplicity
163+
164+
### 2. DiscountController.cs – Create the target endpoint in ServiceB
165+
166+
Create a controller `Controllers/DiscountController.cs` in `ServiceB` that will be invoked by `ServiceA`.
167+
168+
```csharp
169+
[HttpPost("calculate-discount")]
170+
[ProducesResponseType(StatusCodes.Status200OK)]
171+
public async Task<IActionResult> CalculateDiscount([FromBody] Order order)
172+
{
173+
decimal discountInPercentage = 0.0m;
174+
175+
if (order.Quantity >= 12)
176+
{
177+
discountInPercentage = 10.0m;
178+
}
179+
180+
logger.LogInformation("Calculated discount is {Discount}%", discountInPercentage);
181+
182+
return Ok(discountInPercentage);
183+
}
184+
```
185+
186+
### 3. PricingController.cs – Invoke ServiceB from ServiceA
187+
188+
Create a controller `Controllers/PricingController.cs` in `ServiceA` that will invoke `ServiceB` using `DAPR`.
189+
190+
```csharp
191+
[HttpPost("calculate-price")]
192+
public async Task<IActionResult> Calculate([FromBody] Order order)
193+
{
194+
logger.LogInformation("Calculating discount for {Order}", order);
195+
196+
var request = daprClient.CreateInvokeMethodRequest<Order>(
197+
HttpMethod.Post,
198+
"serviceb", // App ID
199+
"/api/discount/calculate-discount",
200+
[],
201+
order);
202+
203+
var response = await daprClient.InvokeMethodAsync<decimal>(request);
204+
205+
var totalPrice = order.PricePerUnit * order.Quantity;
206+
var discountedPrice = totalPrice - (totalPrice / 100.0m * response);
207+
return Ok(discountedPrice);
208+
}
209+
```
210+
211+
**Key points about the invocation:**
212+
213+
- `"serviceb"` is the `DAPR` app ID, not a URL
214+
- `DAPR` handles service discovery automatically
215+
- No need to know the actual host, port, or protocol
216+
- Built-in retries and observability
217+
218+
### 4. Configure Services in AppHost
219+
220+
In `ServiceInvocation.AppHost/Program.cs`, configure both services with `DAPR` sidecars:
221+
222+
```csharp
223+
var builder = DistributedApplication.CreateBuilder(args);
224+
225+
var servicea = builder
226+
.AddProject<Projects.ServiceA>("servicea")
227+
.WithDaprSidecar();
228+
229+
var serviceb = builder
230+
.AddProject<Projects.ServiceB>("serviceb")
231+
.WithDaprSidecar();
232+
233+
builder.Build().Run();
234+
```
235+
236+
**Important configuration details:**
237+
238+
- Each service gets its own `DAPR` sidecar
239+
- `WaitFor(serviceb)` ensures `ServiceB` is ready before `ServiceA` starts
240+
- `DAPR` app IDs are automatically derived from the service names
241+
- No manual URL configuration needed
242+
243+
## Running the Application
244+
245+
Now, when you run your setup, you should see the Aspire dashboard with both services.
246+
247+
![Aspire Dashboard](https://i.ibb.co/MDF4qhsw/service-invocation-aspire-dashboard.png)
248+
249+
### Testing the Service Invocation
250+
251+
- Visit the `ServiceA`'s Swagger endpoint, with `/swagger` at the end of the URL
252+
253+
- Call the `/api/pricing/calculate-price` endpoint with a request body:
254+
255+
**Request**
256+
257+
```json
258+
{
259+
"product": "Banana",
260+
"quantity": 12,
261+
"pricePerUnit": 2.99
262+
}
263+
```
264+
265+
- You'll receive a response like:
266+
267+
**Response**
268+
269+
```json
270+
32.292
271+
```
272+
273+
![API Call](https://i.ibb.co/3m8QbbMV/service-invocation-api-call.png)
274+
275+
### What's Happening Behind the Scenes
276+
277+
When you make this request:
278+
279+
1. `ServiceA` receives your pricing request
280+
2. `ServiceA` uses `DAPR` to invoke `ServiceB` by its app ID (`"serviceb"`)
281+
3. `DAPR`'s sidecar for `ServiceA` discovers `ServiceB`'s location
282+
4. The request is forwarded through `DAPR` sidecars
283+
5. `ServiceB` calculates the discount
284+
6. The response travels back through the sidecars
285+
7. `ServiceA` receives the discount and calculates the final price
286+
287+
```mermaid
288+
sequenceDiagram
289+
participant ServiceA
290+
participant SidecarA as Dapr Sidecar (ServiceA)
291+
participant SidecarB as Dapr Sidecar (ServiceB)
292+
participant ServiceB
293+
294+
ServiceA->>SidecarA: Invoke "serviceb/api/discount/calculate-discount"
295+
SidecarA->>SidecarB: Forward request via service discovery
296+
SidecarB->>ServiceB: HTTP POST /api/discount/calculate-discount
297+
ServiceB-->>SidecarB: Return discount percentage
298+
SidecarB-->>SidecarA: Return discount percentage
299+
SidecarA-->>ServiceA: Return discount percentage
300+
```
301+
302+
All of this happens transparently, with built-in retries, timeouts, and distributed tracing.
303+
304+
## DAPR Service Invocation Benefits
305+
306+
By using DAPR for service invocation, you get several advantages:
307+
308+
### Service Discovery
309+
310+
No hardcoded URLs or ports. Services find each other using app IDs. When services scale or move, DAPR handles the discovery automatically.
311+
312+
- [x] **Resilience**
313+
Built-in retries, timeouts, and circuit breakers without writing custom code.
314+
`DAPR` handles transient failures gracefully.
315+
316+
- [x] **Observability**
317+
Automatic distributed tracing across service calls.
318+
You can see the entire request flow in the `Aspire` dashboard and other observability tools.
319+
320+
#### *Structured Logging*
321+
322+
![Structured Logging](https://i.ibb.co/DHG5sx2T/service-invocation-structured-logs.png)
323+
324+
#### *Distributed Tracing*
325+
326+
![Distributed Tracing](https://i.ibb.co/9kqryTkg/service-invocation-distributed-tracing.png)
327+
328+
- [x] **Security**
329+
Support for mTLS, access control policies, and API token authentication.
330+
Services can communicate securely without manual certificate management.
331+
332+
- [x] Flexibility
333+
Switch between HTTP, gRPC, or other protocols without changing service code.
334+
`DAPR` abstracts the communication layer.
335+
336+
## Summary
337+
338+
With `DAPR` and `Aspire`, you can build resilient, discoverable service-to-service communication with minimal boilerplate.
339+
340+
Instead of managing URLs, retries, and timeouts manually, DAPR handles these concerns automatically — thanks to `DAPR`'s abstraction and `Aspire`'s orchestration.
341+
342+
Your services communicate by app ID, not by URL, making them more maintainable and cloud-ready from day one.

0 commit comments

Comments
 (0)