Designing Scalable Microservices with .NET 9: Overview
The distributed microservices architecture won the game in corporate software development. And that’s mainly due to the corporation’s natural way of distributing people into small, manageable teams. Hence, by building a microservices architecture, each team can be responsible for only smaller pieces of the business product instead of the whole thing.
With that in mind, it’s crucial to understand what the key factors are when building a microservices architecture:
- Scalability: Each service needs to operate normally under increasing web traffic load.
- Loose Coupling: Services should be independently deployable and manageable by a single development team.
- Security: Web calls are at the core of microservices architecture, so it is essential to develop a secure web flow that protects data, identity, and access to services.
- Observability: Each service should implement its own observability and monitoring flows, using metrics and alerts to give the company visibility about the service’s correct functioning.
Most of these aspects are built into .NET 9, facilitating rapid development using the .NET microservices architecture’s best practices. For instance, .NET 9 supports Minimal APIs, native gRPC and HTTP/3 communication protocols, and web server optimizations to handle high-throughput web workloads.
Additionally, the DAPR SDK and interface-based dependency injection pattern help build more cohesive and manageable code and expose the service’s behaviour to other clients. Hence, they help create more independent and loosely coupled services.
Other .NET 9 features, such as default HTTPS in web calls, OAuth2 and OIDC support, authorization filters, and IdentityServer, help developers develop secure web flows more easily.
Finally, OpenTelemetry’s built-in tracing and logging observability facilitates the creation of dashboards, alerts, and logs to visualize useful information about system behaviour.
Also, make sure you know precisely the costs associated with migrating and maintaining a cloud application.
Microservices API Building Blocks in .NET 9
One crucial aspect of building a .NET microservices architecture is developing web APIs to work as the communication bridge between microservices. Hence, to create a new business feature in such a software architecture, we can create one or more APIs responsible for one thing. Then, we can connect all of them via web calls using some communication protocol such as HTTP or gRPC to produce the final result for the end client.
This section lists a few .NET 9 features that help developers create optimized web APIs easily.
Minimal APIs and Endpoint Filters
The Minimal API feature in .NET 9 allows developers to minimize the process of creating a new endpoint. Instead of defining models and attributes, controllers, and other boilerplate code, we can simply define a function entry in a .cs file containing the API resource URI, response, and request bodies, as well as headers. That increases development speed and reduces the needed dependencies, resulting in a faster build time.
Endpoint filters let developers associate middleware filters with minimal API definitions to perform tasks such as body validation, authorization, and logging. For instance, we can create a separate file for every function in the minimal API definition file to perform the entire request body validation. That helps separate the business logic concerns from other maintenance tasks.
gRPC & REST Coexistence
gRPC and REST communication protocols can coexist in the same .NET 9 web API microservice’s Kestrel server, which was not a feature in previous versions of .NET and is uncommon in other frameworks. This is helpful since each protocol best fits a scenario. For instance, gRPC is a more contract-based protocol that uses a reduced size of the response and request bodies. On the other hand, REST is a more flexible protocol that allows any fields to exist in request and response bodies without a specific agreement between client and server.
Both are useful for different use cases. gRPC is typically used for internal, optimized calls, while REST is used for user-facing APIs. The good part is that we can use both of them out of the box in a .NET 9 application.
Built-In Design Patterns and External Integrations in .NET 9
.NET 9 also has features that facilitate some patterns integrations, such as CQRS, Event-Driven Design, API Gateways, Service Discovery, Saga, and Circuit Breakers. Let’s look at each briefly in this section.
CQRS in Microservices: Clean, Scalable Write/Read Separation
The CQRS in the microservices pattern segregates write and read services, databases, and other infrastructure. Typically, this helps to separate responsibilities and optimize database access in scenarios where the ratio of reads to writes is higher or vice versa. In CQRS, we separate read actions into query items and write actions into command items.
With some built-in features of the .NET 9 microservices architecture, producing the necessary parts required by CQRS is easier. For instance, the MediatR dependency encourages the developer to create separate classes for different types of handling on the input, like reading and writing, which essentially encourages the development of code that adheres to the CQRS pattern.
CQRS also states that an object’s state after storage is immutable. In other words, after saving a record via a command in CQRS, it should not be modified afterwards. Hence, in .NET 9, the Records feature lets developers create immutable objects more easily, which also helps to recreate the pattern in code.
Implementing Event-Driven Microservices Architecture in .NET 9
Event-driven systems let the system communicate asynchronously. Thus, instead of calling REST or gRPC APIs, we leverage queues, streams, and message broker systems. This can be beneficial in certain situations, such as reacting to a situation instead of acting. For instance, if the user receives money into his bank account, it’s useful to send him a notification about this. Thus, we can react to the event “receives money” and act by sending a notification through an API call, for example.
Additionally, event-driven architectures are more fault-tolerant than usual architectures since they decouple the failures from the server and let the event be reprocessed. For instance, if the payment notification fails, the event goes back to the end of the queue to be reprocessed later.
Finally, using event-driven architectures ensures that a reduced traffic spike reaches our web servers since only a single event is processed at a time. That increases scalability by avoiding situations where a huge number of requests suddenly spikes into a web server, leading to a crash.
We have a wide set of tools for implementing event-driven microservices architecture in .NET 9, such as Kafka broker, RabbitMQ, and Azure Event Grid integrations.
Other Built-In Design Patterns in .NET 9
There are other microservice design patterns present in the .NET 9 framework that help to create more secure, scalable, fault-tolerant, and consistent web applications, for instance:
- Steeltoe is a dependency in .NET native apps that provides the building blocks for integration with dynamic registration and service discovery tools such as Netflix’s Eureka, HashiCorp Consul, Kubernetes, and Spring Cloud. Service discovery is fundamental in a microservices architecture, as each service must know the exact IP address location of other services in order to communicate with them properly.
- YARP is a Microsoft official tool that implements the reverse proxy pattern and supports API routing, authentication, load balancing, and rate-limiting. These features are necessary for security and scalability in a .NET microservices architecture.
- For resiliency and fault tolerance, .NET 9 natively integrates with Polly, a tool that implements web call circuit breakers, retry, and timeout mechanisms.
- Finally, for consistency, .NET 9 has dependencies that help to create state machines, compensation, orchestration paradigms, and web transaction compensation. For instance, we can use MassTransit to create customized workflows like a saga. Consistency is crucial in a microservices architecture to keep different data stores synced with the correct state.
Securing .NET 9 Microservices with NET-Security and Identity
One of the most crucial and often neglected aspects of building .NET microservices is data security, API access security, and user identity security. To address these issues, the .NET 9 ecosystem offers features such as role/claim-based authorization models, identity servers, and OAuth2 / OpenID integrations.
Role-Based vs. Claim-Based Authorizations
Role-based authorization is the tactic of giving access to a resource, like an API or database, based on a user’s role. For instance, users in the group “admin” might have more permissions, like writing or deleting a resource, than users in the group “external-user”. Hence, each user in the system is assigned to a group by an administrator, and it’s the responsibility of the authentication flow to identify the principal (the user trying to access) and block or grant their access.
On the other hand, claim-based authorization picks an object named “claim” containing the user’s own statement about their permissions. So, instead of having a storage containing the users and their roles, like in a role-based model, we depend on the information the requester sends via a claim. For instance, a user wants to access an API and sends a claim with cryptography information about their email address and name. We give him access or not based on internal rules that use these fields.
Both authorization models are useful for different situations to achieve security in a microservices architecture. When building microservices with .NET security and identity flows, it’s easy to develop both authorization models with one-liners in the code, as shown in the claim-based auth docs and role-based auth docs.
IdentityServer, OAuth2 & OpenID Connect
After implementing an authentication model, like claim-based or role-based, in the code, we need a secure system of record to verify the request and provide the authentication. This system of record is typically an identity server that receives requests from a principal and authorizes or provides identification tokens, such as JWT, based on claims.
.NET 9 provides the necessary building blocks, cryptographic algorithms, and security best practices to implement an identity server correctly. For instance, the built-in Microsoft official tool is the .NET core identity server, which has all the necessary parts, like OAuth2 and OIDC integrations, support for both claim-based and role-based models, and stable cryptographic algorithms. Alternatively, the Duende dependency is a more modern identity server with the same capabilities as the .NET Core identity server.
Therefore, aligning both the identity server and the client authentication model in code ensures correct security in web calls between microservices. Additionally, we can put the authentication flow in an API gateway pattern to increase security and scalability. That helps to stop unauthenticated requests from going through the internal APIs, masking internal resources, and saving processing time.
Observability and Monitoring in .NET 9
The final aspect we’ll cover in this article is how to successfully deploy a scalable .NET microservices architecture into production, which is observability. This is a crucial process to have visibility into technical metrics, user behaviour when interacting with the system, malfunctioning services, and others.
Logs and user metrics are essential to understanding how our services respond to user requests. For instance, we can create dashboards that summarize how many users enjoy the newly deployed user experience based on criteria like time spent on the page or process time for a new button. For that, .NET 9 has native tools that give access to OpenTelemetry, the market-standard technology for observability. Additionally, it has dependencies such as Serilog that facilitate and encourage the structured logging approach.
Additionally, .NET 9 has distributed-tracing tools, such as OpenTelemetry’s Baggage, that propagate the context of user requests throughout the entire request lifespan. This is crucial to understanding where the request is failing or taking more time to process and identifying single points of failure or other pain points in a .NET microservices architecture.
Finally, we can attach alerting tools to our OpenTelemetry metrics to send real-time alerts to the engineering team if something malfunctions. For instance, if a service is lagging, we can check the latency metric and show an alert like “Latency is higher than 2000ms in service A” to the entire team or individual engineers. With OpenTelemetry, it’s easy to integrate metrics with alerting tools such as Grafana or ElasticSearch.
Microservices with .NET 9: From Development to Production
After looking at all the crucial aspects of designing and building microservices with .NET 9, it might seem like a lot of work. And it is! From the architectural design by choosing the correct infrastructure, API resource names, to developing applications with scalability, security, high availability, and fault-tolerance in mind. It’s a lot of work, especially if the business is not a specialist in cloud-distributed solutions like .NET microservices.
Thus, hiring the top 1% .NET developers from consultants who have delivered successful .NET microservice architectures is vital. Chudovo has delivered many successful cloud projects in the past, and many customized .NET solutions for different types of industries. That makes them a very recommended partner to include in your journey to microservices!