Posted on • Originally published atnikiforovall.github.io on
Managing Startup Dependencies in .NET Aspire
TL;DR
This article demonstrates how to utilize .NET Aspire as an orchestrator. You will discover how effortless it is to define a dependency graph between components during startup.
Source code :https://github.com/NikiforovAll/aspire-depends-on
Example :https://github.com/NikiforovAll/aspire-depends-on/tree/main/samples/Todo
Table of Contents:
- TL;DR
- Introduction
- Using docker-compose 🐳
- Why .NET Aspire? 🚀
- Using Aspire
- Conclusion
- References
Introduction
Managing startup dependencies is a common task for any kind of a project in modern days, especially in microservices solutions.
☝️ Here are a few reasons why it is important:
Order of Initialization : Multiple services may need to be initialized and started in a specific order. For example, if Service A depends on Service B, Service B needs to be up and running before Service A can start. By managing startup dependencies, you ensure that services are initialized in the correct order, preventing potential issues and ensuring smooth operation and startup.
Graceful Startup and Shutdown : Managing startup dependencies allows you to implement graceful startup and shutdown procedures. During startup, you can perform necessary initialization tasks, such as establishing database connections or loading configuration settings, before accepting incoming requests. Similarly, during shutdown, you can gracefully stop services, release resources, and perform any necessary cleanup operations.
Fault Tolerance and Resilience : In a distributed environment,failures are inevitable. By managing startup dependencies, you can implement fault tolerance and resilience mechanisms. For example, you can configure services to retry connecting to dependent services if they are temporarily unavailable. This helps ensure that your application can recover from failures and continue functioning properly.
To manage startup dependencies effectively, you can use various techniques and tools.Orchestrators can help automate the process of managing dependencies and ensure that services are started in the correct order.
Using docker-compose 🐳
One common approach is by usingdocker-compose
.It provides a declarative way to define the dependencies between components and ensure they are started in the correct order.
To illustrate , let’s assume that theapi
service represents a todo application that relies on apostgres
database. Before theapi
service can start accepting requests, it needs to ensure that the necessary database migration (migrator
) and seeding operations have been completed.
Once the migration is successfully completed, theapi
service can start and begin serving requests. This approach allows for a controlled and orderly startup process, ensuring that all dependencies are satisfied before theapi
service becomes available.
version:'3.8'services:postgres:image:postgres:latestdepends_on:-migratorpgadmin:image:dpage/pgadmin4:latestmigrator:image:migrate/migrate:latestdepends_on:-postgres
Thisdocker-compose.yml
file defines the servicespostgres
,pgadmin
, andmigrator
. Thepostgres
service is the database, thepgadmin
service is a web-based administration tool, and themigrator
service is responsible for performing the database migration.
By specifying the dependencies using thedepends_on
keyword, themigrator
service will wait for thepostgres
service to be up and running before executing the migration. Once the migration is completed, theapi
service can safely start and interact with the database.
version:'3.8'services:postgres:image:postgres:latestdepends_on:-migratorpgadmin:image:dpage/pgadmin4:latestmigrator:image:migrate/migrate:latestdepends_on:-postgres
🤔💭 But, wait, is it that easy? Why do we need to using something else than?
Well… In practice, relying solely on thedepends_on directive in a Docker Compose file may not be sufficient to ensure that a component has fully started up and is ready to accept connections or perform its intended tasks. Whiledepends_on helps manage the startup order of services, itdoes not guarantee that a service is fully operational before another service starts.
To address this, it is common to write custom bash scripts or use other tools to perform health checks on the dependent services. These health checks can verify that the service is in a desired state before proceeding with the startup of other components.
A health check typically involves making requests to the dependent service and checking for specific responses or conditions that indicate it is ready. For example, you might check if a database service is accepting connections by attempting to connect to it and verifying that it responds with a successful connection status.
It’s important to note that the specific implementation of health checks and custom scripts may vary depending on the technology stack and tools you are using.
Why .NET Aspire? 🚀
While managing startup dependencies using YAML, Dockerfile, and Bash scripts can be challenging, .NET Aspire offers a better way to handle these dependencies directly in C#.
With .NET Aspire, you can define a dependency graph between components during startup using C# code. This allows for a more intuitive and seamless integration with your .NET projects. You can easily specify the order of initialization, implement graceful startup and shutdown procedures, and ensure fault tolerance and resilience.
By leveraging the power of C#, you have access to the rich ecosystem of .NET libraries and frameworks, making it easier to handle complex startup scenarios. You can use the built-in dependency injection capabilities of .NET to manage and resolve dependencies, ensuring that services are initialized in the correct order.
Overall, .NET Aspire offers a more developer-friendly approach to managing startup dependencies in .NET projects, making it a compelling choice for simplifying your application’s startup.
Using Aspire
I’ve prepared Nuget package calledNall.Aspire.Hosting.DependsOn to simplify the process of defining dependencies between components.
Let’s see how to use it to describe dependencies for the Todo application described above.
Here is our starting point,without dependencies :
varbuilder=DistributedApplication.CreateBuilder(args);vardbServer=builder.AddPostgres("db-server");dbServer.WithPgAdmin(c=>c.WithHostPort(5050));vardb=dbServer.AddDatabase("db");varmigrator=builder.AddProject<Projects.MigrationService>("migrator").WithReference(db);varapi=builder.AddProject<Projects.Api>("api").WithReference(db);builder.Build().Run();
💡 I strongly recommend you checking out source code for more details, it is a fully-functional application. All you need to do - is to run it by hittingF5
-https://github.com/NikiforovAll/aspire-depends-on/tree/main/samples/Todo
Now, let’s install required packages.Nall.Aspire.Hosting.DependsOn
defines aWaitFor
andWaitForCompletion
methods used to determine startup ordering.
Install core package 📦:
dotnet add package Nall.Aspire.Hosting.DependsOn
Also, we need to install specific packages for the dependencies. In our case, it is a PostgreSQL database.
Install health-check-specific package 📦:
dotnet add package Nall.Aspire.Hosting.DependsOn.PostgreSQL
Specific health checks packages defineWithHealthCheck
extension methods. These methods are technology specific, but in essence are easy to understand and could be written on demand for any kind of dependency without usingNall.Aspire.Hosting.DependsOn
. Later, I will show you how to write your ownWithHealthCheck
method.
Let’s put everything together:
varbuilder=DistributedApplication.CreateBuilder(args);vardbServer=builder.AddPostgres("db-server").WithHealthCheck();// <-- define health checkdbServer.WithPgAdmin(c=>c.WithHostPort(5050).WaitFor(dbServer));// <-- wait for dbvardb=dbServer.AddDatabase("db");varmigrator=builder.AddProject<Projects.MigrationService>("migrator").WithReference(db).WaitFor(db);// <-- wait for dbvarapi=builder.AddProject<Projects.Api>("api").WithReference(db).WaitForCompletion(migrator);// <-- wait until process is terminatedbuilder.Build().Run();
Writing Custom Health Checks for Aspire
By convention,WithHealthCheck
method should defineHealthCheckAnnotation
annotation for Aspire resource.
publicclassHealthCheckAnnotation(Func<IResource,CancellationToken,Task<IHealthCheck?>>healthCheckFactory):IResourceAnnotation
Let’s see the implementation ofNall.Aspire.Hosting.DependsOn.Uris
, the package that allows to wait for resources that expose a health check endpoint, usually it is/health
endpoint that returns “200 OK” status code if everything is fine.
Here isWithHealthCheck
method signature:
publicstaticIResourceBuilder<T>WithHealthCheck<T>(thisIResourceBuilder<T>builder,string?endpointName=null,stringpath="health")whereT:IResourceWithEndpoints
Note, usually, you don’t really need to implement health checking logic,AspNetCore.HealthChecks.* has something to offer.
In case ofNall.Aspire.Hosting.DependsOn.Uris
, it depends onAspNetCore.HealthChecks.Uris
.
So the implementation is straight forward, just find a endpoint by name and define health check for a given path:
publicstaticIResourceBuilder<T>WithHealthCheck<T>(thisIResourceBuilder<T>builder,string?endpointName=null,stringpath="health")whereT:IResourceWithEndpoints{returnbuilder.WithAnnotation(newHealthCheckAnnotation(async(resource,ct)=>{if(resourceisnotIResourceWithEndpointsresourceWithEndpoints){returnnull;}varendpoint=endpointNameisnull?resourceWithEndpoints.GetEndpoints().FirstOrDefault(e=>e.Schemeis"http"or"https"):resourceWithEndpoints.GetEndpoint(endpointName);varurl=endpoint?.Url;varoptions=newUriHealthCheckOptions();options.AddUri(new(new(url),path));varclient=newHttpClient();returnnewUriHealthCheck(options,()=>client);}));}
Conclusion
In this article, we explored the challenges of managing startup dependencies in modern applications, particularly in the context of microservices architectures.
.NET Aspire elevates the management of startup dependencies. By allowing developers to define dependency graphs in C#.
Through practical examples, we demonstrated how to use .NET Aspire and its extensions to manage dependencies in a more intuitive and reliable manner. We also touched on the creation of custom health checks, showcasing the flexibility and extensibility of .NET Aspire.
🙌 In conclusion, managing startup dependencies has never been easier with .NET Aspire, and I believe every project needs it.
References
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse