I am still learning and afraid of captive dependency (a dependency with an incorrectly configured lifetime). Consider the following code snippet and the full code is given at the bottom.
My objectives:
- organize Minimal Api endpoints with endpoint classes,
- register the endpoint classes with DI (but I am not sure their lifetime),
- resolve the endpoint classes with looping and invoke their
RegisterEndpoints()
(again I am not sure whether they must be inCreateScope()
or not)
public abstract class BaseEndpoints
{
protected BaseEndpoints(string path, ILogger logger){/**/}
public abstract void RegisterEndpoints(WebApplication app);
}
public sealed class ProductEndpoints : BaseEndpoints
{
public ProductEndpoints(ILogger<ProductEndpoints> logger, AppDbContext db)
: base("/api/v1/product", logger){/**/}
public override void RegisterEndpoints(WebApplication app)
{
app.MapGet(path, GetAll);
app.MapGet(path + "/{id:int}", Get);
}
private async ValueTask<IResult> GetAll(){/**/}
private async ValueTask<IResult> Get(int id){/**/}
}
builder.Services.AddDbContext<AppDbContext>(o => o.UseInMemoryDatabase("AppDbContext"));
// Endpoints should have transient lifetime, right?
builder.Services.AddTransient<BaseEndpoints, ProductEndpoints>();
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var ctx = scope.ServiceProvider.GetService<AppDbContext>();
ctx?.Database.EnsureCreated();
// endpoint registration should be here or outside this scope?
foreach (var endpoint in scope.ServiceProvider.GetServices<BaseEndpoints>())
{
endpoint.RegisterEndpoints(app);
}
// app.Run() should be here or outside this scope?
app.Run();
}
Question:
- What is the correct lifetime for endpoint classes when registered with DI? In my understanding, they should be transient because they are used just once during application startup.
- Where should the endpoints services be resolved? Inside
CreateScope()
or outside? - Where should I invoke
app.Run()
? InsideCreateScope()
or outside?
Minimal Working Example
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Services.AddDbContext<AppDbContext>(o => o.UseInMemoryDatabase("AppDbContext"));
// Endpoints should have transient lifetime, right?
builder.Services.AddTransient<BaseEndpoints, ProductEndpoints>();
// builder.Services.AddTransient<BaseEndpoints, OtherEndpoints>();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
using (var scope = app.Services.CreateScope())
{
var ctx = scope.ServiceProvider.GetService<AppDbContext>();
ctx?.Database.EnsureCreated();
// endpoint registration should be here or outside this scope?
foreach (var endpoint in scope.ServiceProvider.GetServices<BaseEndpoints>())
{
endpoint.RegisterEndpoints(app);
}
// app.Run() should be here or outside this scope?
app.Run();
}
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Product> Products => Set<Product>();
protected override void OnModelCreating(ModelBuilder mb)
{
base.OnModelCreating(mb);
mb.Entity<Product>().HasData(
new Product { Id = 1, Name = "Allen Keys" },
new Product { Id = 2, Name = "Bolts" },
new Product { Id = 3, Name = "Cutters" }
);
}
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = default!;
}
public abstract class BaseEndpoints
{
protected readonly string path;
protected readonly ILogger logger;
protected BaseEndpoints(string path, ILogger logger)
{
this.path = path;
this.logger = logger;
}
public abstract void RegisterEndpoints(WebApplication app);
}
public sealed class ProductEndpoints : BaseEndpoints
{
private readonly AppDbContext db;
public ProductEndpoints(ILogger<ProductEndpoints> logger, AppDbContext db)
: base("/api/v1/product", logger)
{
this.db = db;
}
public override void RegisterEndpoints(WebApplication app)
{
app.MapGet(path, GetAll);
app.MapGet(path + "/{id:int}", Get);
// Other app.Map*() invocations go here.
}
private async ValueTask<IResult> GetAll()
{
return await Task.FromResult(TypedResults.Ok(db.Products));
}
private async ValueTask<IResult> Get(int id)
{
if (await db.Products.FindAsync(id) is Product p)
{
return TypedResults.Ok(p);
}
else
{
return TypedResults.NotFound();
}
}
}