Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Oleksii Nikiforov
Oleksii Nikiforov

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:

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.

graph TD; postgres --> pgAdmin; postgres --> migrator; migrator --> api;

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
Enter fullscreen modeExit fullscreen mode

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
Enter fullscreen modeExit fullscreen mode

🤔💭 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();
Enter fullscreen modeExit fullscreen mode

💡 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
Enter fullscreen modeExit fullscreen mode

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
Enter fullscreen modeExit fullscreen mode

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();
Enter fullscreen modeExit fullscreen mode

Writing Custom Health Checks for Aspire

By convention,WithHealthCheck method should defineHealthCheckAnnotation annotation for Aspire resource.

publicclassHealthCheckAnnotation(Func<IResource,CancellationToken,Task<IHealthCheck?>>healthCheckFactory):IResourceAnnotation
Enter fullscreen modeExit fullscreen mode

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
Enter fullscreen modeExit fullscreen mode

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);}));}
Enter fullscreen modeExit fullscreen mode

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)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Software Engineer at EPAM. Interested in .NET
  • Work
    Software Engineer
  • Joined

More fromOleksii Nikiforov

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp