ASP.NET Core 6: Minimal APIs, two reasons why I can’t do without it so far
Introduction
A few days ago I introduced you to the minimal APIs, in this blog post: https://anthonygiretti.com/2021/08/12/asp-net-core-6-working-with-minimal-apis/
They are an evolution of the Route to Code concept which was already an interesting concept in ASP.NET Core. I described how it works here: https://anthonygiretti.com/2020/06/29/nano-services-with-asp-net-core-or-how-to-build-a-light-api/
Today I will explain to you the two reasons why I can not do without it
Quick and easy exposure of protobuf files via REST
None of my gRPC projects fail to use the exposure of protobufs files with minimal APIs anymore, it’s so easy to use! The following service is used to list protobuf files and their version, download a protobuf file or display the content of a protobuf file in a browser:
namespace CountryService.gRPC.Services; | |
public class ProtoService | |
{ | |
private readonly string _baseDirectory; | |
public ProtoService(IWebHostEnvironment webHost) | |
{ | |
_baseDirectory = webHost.ContentRootPath; | |
} | |
public Dictionary<string, IEnumerable<string>> GetAll() | |
{ | |
return Directory.GetDirectories($"{_baseDirectory}/protos") | |
.Select(x => new { version = x, protos = Directory.GetFiles(x).Select(Path.GetFileName) }) | |
.ToDictionary(o => Path.GetRelativePath("protos", o.version), o => o.protos); | |
} | |
public string Get(int version, string protoName) | |
{ | |
var filePath = $"{_baseDirectory}/protos/v{version}/{protoName}"; | |
var exist = File.Exists(filePath); | |
return exist ? filePath : null; | |
} | |
public async Task<string> ViewAsync(int version, string protoName) | |
{ | |
var filePath = $"{_baseDirectory}/protos/v{version}/{protoName}"; | |
var exist = File.Exists(filePath); | |
return exist ? await File.ReadAllTextAsync(filePath) : string.Empty; | |
} | |
} |
Then, by dependency injection I’m able to expose them easily on a minimal endpoint:
var builder = WebApplication.CreateBuilder(args); | |
// Add services to the container. | |
builder.Services.AddGrpc(options => { | |
options.EnableDetailedErrors = true; | |
options.IgnoreUnknownServices = true; | |
options.MaxReceiveMessageSize = 6291456; // 6 MB | |
options.MaxSendMessageSize = 6291456; // 6 MB | |
options.CompressionProviders = new List<ICompressionProvider> | |
{ | |
new BrotliCompressionProvider() // br | |
}; | |
options.ResponseCompressionAlgorithm = "br"; // grpc-accept-encoding | |
options.ResponseCompressionLevel = CompressionLevel.Optimal; // compression level used if not set on the provider | |
options.Interceptors.Add<ExceptionInterceptor>(); // Register custom ExceptionInterceptor interceptor | |
}); | |
builder.Services.AddGrpcReflection(); | |
builder.Services.AddScoped<ICountryRepository, CountryRepository>(); | |
builder.Services.AddScoped<ICountryServices, CountryServices>(); | |
builder.Services.AddSingleton<ProtoService>(); | |
builder.Services.AddDbContext<CountryContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("CountryService"))); | |
var app = builder.Build(); | |
app.MapGrpcReflectionService(); | |
app.MapGrpcService<CountryGrpcService>(); | |
app.MapGet("/protos", (ProtoService protoService) => | |
{ | |
return Results.Ok(protoService.GetAll()); | |
}); | |
app.MapGet("/protos/v{version:int}/{protoName}", (ProtoService protoService, int version, string protoName) => | |
{ | |
var filePath = protoService.Get(version, protoName); | |
if (filePath != null) | |
return Results.File(filePath); | |
return Results.NotFound(); | |
}); | |
app.MapGet("/protos/v{version:int}/{protoName}/view", async (ProtoService protoService, int version, string protoName) => | |
{ | |
var text = await protoService.ViewAsync(version, protoName); | |
if (!string.IsNullOrEmpty(text)) | |
return Results.Text(text); | |
return Results.NotFound(); | |
}); | |
// Custom response handling over ASP.NET Core middleware | |
app.Use(async (context, next) => | |
{ | |
if (!context.Request.Path.Value.Contains("protos/v")) | |
{ | |
context.Response.ContentType = "application/grpc"; | |
context.Response.Headers.Add("grpc-status", ((int)StatusCode.NotFound).ToString()); | |
} | |
await next(); | |
}); | |
// Run the app | |
app.Run(); |
What I love above all is the efficiency of the static Results class. With that class I can expose json data easily with the Ok() method, expose file content with the Text() method or provide file download with the File() method.
Example with File() method, I’m able to import protobuf via the Connected Services mnu in Visual Studio 2022:


Quick Entity Framework Core dbContext testing
Who doesn’t like Entity Framework Core? I adore. and you know I love it too? to be able to quickly test if my dbContext I have configured it correctly, suddenly I want to quickly test if with a DbSet I can make requests to the database, you never know I may have done something wrong? Well the minimal APIs, still through dependency injection, allow me to inject my Dbcontext and test a LINQ request and see if everything works correctly:
var builder = WebApplication.CreateBuilder(args); | |
builder.Services.AddDbContext<CountryContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("CountryService"))); | |
var app = builder.Build(); | |
app.MapGet("/countries", (CountryContext dbContext) => | |
{ | |
return Results.Ok(dbContext.Countries.Select(x => new | |
{ | |
x.Name, | |
x.Description, | |
x.CapitalCity, | |
x.Anthem, | |
SpokenLagnuages = x.CountryLanguages.Select(y => y.Language.Name) | |
})); | |
}); | |
// Run the app | |
app.Run(); |
Example with Postman:

Practical isn’t it ?