As I work more consistently with the top-level style of .NET and ASP.NET Core applications, I find myself needing to do more start-up operations that have to occur before my application starts. Typically, in an ASP.NET Core application, the server is ready to begin accepting requests after a call to app.Run
method or one of its variants. Before starting the server, you may want to execute a wake-up call to a third-party service, run database migrations, log messages to your configured syncs, and so on. The list of start-up tasks is infinite. There’s just one problem. Most dependencies I use are registered as Scoped
and require a scope instance to resolve. The scope requirement makes it challenging to use them in the static context of a top-level statement file.
This post will look at an approach to use methods to run scoped-dependent logic while utilizing all the pre-existing registered dependencies in your application, regardless of the scope lifetime.
The Common Workaround
As someone in developer advocacy, I aim to write demos that run without many ceremonies. You can clone a repository and know you’ll run the code within seconds, not hours. It takes some thought to accomplish the goal for each demo project.
One common need arises in data-driven demos using Entity Framework Core. Typically, the steps required are as follows:
- Create a new scope from the
IServiceProvider
- Resolve all the required dependencies
- Execute the migration scripts and initialize data
- Dispose of all dependencies within the scope or transient lifetime
Let’s look at a typical example.
var app = builder.Build();
// initialize database
using (var scope = app.Services.CreateScope()) {
var db = scope.ServiceProvider.GetRequiredService<VehiclesContext>();
await VehiclesContext.InitializeAsync(db);
}
Wouldn’t it be nicer to be able to replace our code with the following single line?
await app
.Services
.ExecuteAsync(VehiclesContext.InitializeAsync);
Well, I’ve implemented such an extension method for you to use in your code base.
The Solution To Single-line Initializers
The first step to the solution is realizing that .NET allows lambdas to collapse into method groups. So let’s look at two equivalent lines of code.
// regular lambda
await app
.Services
.ExecuteAsync((VehiclesContext db) =>
VehiclesContext.InitializeAsync(db));
// method group
await app
.Services
.ExecuteAsync(VehiclesContext.InitializeAsync);
A method group occurs when the parameters to the lambda expression match the same parameters as the method being invoked in the expression body. With the use of using static
, we can reduce the visual footprint even further.
// using static BoxedSoftware.Models.VehiclesContext;
await app.Services.ExecuteAsync(InitializeAsync);
We all like more signal-to-noise, right? So how does it work? First, let’s look at the implementation of my extension method.
public static class ServiceProviderExtensions
{
public static async Task ExecuteAsync(
this IServiceProvider serviceProvider,
Delegate @delegate,
CancellationToken cancellationToken = default)
{
using var scope = serviceProvider.CreateScope();
var args = GetArguments(scope, @delegate, cancellationToken);
var result = @delegate.DynamicInvoke(args.ToArray());
if (result is Task task) {
await task;
}
}
public static void Execute(
this IServiceProvider serviceProvider,
Delegate @delegate)
{
using var scope = serviceProvider.CreateScope();
var args = GetArguments(scope, @delegate);
@delegate.DynamicInvoke(args);
}
private static object?[] GetArguments(
IServiceScope scope,
Delegate @delegate,
CancellationToken? cancellationToken = default)
{
var method = @delegate.Method;
var parameters = method.GetParameters();
var args = new List<object?>(parameters.Length);
foreach (var parameterInfo in parameters)
{
var type = parameterInfo.ParameterType;
if (type == typeof(CancellationToken))
{
// likely the last parameter
args.Add(cancellationToken);
}
else
{
var arg = scope.ServiceProvider.GetService(type);
args.Add(arg);
}
}
return args.ToArray();
}
}
I utilize the Delegate
type to get the MethodInfo
of any lambda expression passed to my implementation. From there, I can get the parameters and resolve each dependency based on the ParameterType
given that the IServiceProvider
interface allows me to pass in a Type
. The solution’s complexity level is relatively low to its value.
The solution also has synchronous and asynchronous options that allow you to pass in a CancellationToken
if you need to cancel an operation. Additionally, the async\await
version will block until the process is complete, ensuring all start-up tasks are ready before the ASP.NET Core application begins accepting requests.
Cool, right?!
Finally, the scope and all its dependencies are disposed of once the Execute
or ExecuteAsync
is complete. Using the IServiceScope
instance also allows me to access dependencies that I register as Transient
or Scoped
. In the case of my example, the Entity Framework Core DbContext
is registered as Scoped
and can only be accessed from an IServiceScope
instance.
Conclusion
There you have, with a straightforward extension method, the use of Delegate
and IServiceScope
, we now can run start-up code with a one-liner in our Program.cs
files. The implementations of Execute
and ExecuteAsync
also support local functions, so you can define those in line and still use the methods. I like defining static methods closer to the types and using them as method groups, but you could also pass a lambda expression defined inline to reduce jumping around.
So, what do you think? Would you use this in your .NET top-level statement apps? Which approach do you like better (local functions, static methods, or inline)?
Let me know by following me on Twitter at @buhakmeh. As always, thank you for reading.