Azure Functions are a popular choice for building serverless, event-driven applications. However, as applications grow in complexity, developers often encounter challenges that require greater control and efficiency. Furthermore, the frequent updates to Azure Functions APIs, including the upcoming deprecation of the in-process worker model in November 2026, make continuous upgrades cumbersome. For business-critical applications, ensuring zero downtime during these transitions can be especially challenging.
In this post, we'll explore how to migrate business-critical Azure Function calls to Minimal API, leveraging FastEndpoints to simplify the transition. While FastEndpoints is the focus, the design concepts discussed here can be applied even without relying on specific packages.
Why Migrate to Minimal API?
Azure Functions are excellent for quick, lightweight serverless operations, but they can present limitations for complex, business-critical applications. Here are some reasons to consider transitioning to Minimal API:
- Performance Boost: Minimal APIs are optimized for low latency and high throughput.
- Improved Structure: FastEndpoints offers clear, organized handling of API routes and processes, helping maintain clean separation of concerns.
- Enhanced Control: Minimal APIs provide developers with fine-grained control over dependency injection, middleware, and other components.
Comparing Azure Functions to Minimal API with FastEndpoints
To illustrate the differences, let’s compare a simple Azure Function to an equivalent implementation using Minimal API and FastEndpoints.
Azure Function Example
[FunctionName("MyFunction")]
public async Task<IActionResult> Run(
[HttpTrigger(
AuthorizationLevel.Function,
"post",
Route = "demo/{RouteId}/{SubId}")]
HttpRequest req,
string RouteId,
string SubId,
ILogger log)
{
log.LogInformation("Processing a request...");
// Validate RouteId
if (!Guid.TryParse(RouteId, out Guid routeId))
{
return new BadRequestObjectResult(new ProblemDetails
{
Title = "Invalid RouteId",
Status = StatusCodes.Status400BadRequest,
Detail = "RouteId must be a valid GUID"
});
}
// Validate SubId
if (!Guid.TryParse(SubId, out Guid subId))
{
return new BadRequestObjectResult(new ProblemDetails
{
Title = "Invalid SubId",
Status = StatusCodes.Status400BadRequest,
Detail = "SubId must be a valid GUID"
});
}
// Extract CompanyId from JWT token
var tokenHandler = new JwtSecurityTokenHandler();
var token = req.Headers["Authorization"].ToString().Split(" ")[1];
var companyId = Guid.Parse(
tokenHandler.ReadJwtToken(token)
.Claims.First(c => c.Type == "YourCompanyIdClaim")
.Value);
return new OkObjectResult("Hello from Azure Function");
}
This implementation directly interacts with HttpRequest
, leaving validation and structure to the developer. While flexible, it lacks the strong typing and modularity of more modern approaches.
Minimal API Example with FastEndpoints
public class MyEndpoint : Endpoint<MyRequest>
{
public override void Configure()
{
// Define the route and HTTP method
Post("demo/{RouteId}/{SubId}");
}
public override async Task HandleAsync(
MyRequest req, CancellationToken ct)
{
// Return a response directly
await SendOkAsync("Hello from Minimal API", ct);
}
}
// Model defining expected input
public record MyRequest
{
Guid RouteId { get; init; }
Guid SubId { get; init; }
// Automatically bind claim from JWT token
[FromClaim("YourCompanyIdClaim")]
Guid CompanyId { get; init; }
}
Here, the main action happens in the HandleAsync
method, while the MyRequest
object handles model binding. FastEndpoints simplifies validation and dependency injection, reducing boilerplate code and improving maintainability.
Breaking Down the Key Differences
Comparing the two approaches side by side reveals key differences. Let’s explore how these differences impact structure, developer experience, and flexibility:
Validation
Validation occurs before the main handler in FastEndpoints, ensuring only valid models reach the core logic. For instance:
public class MyRequestValidator : Validator<MyRequest>
{
public MyRequestValidator()
{
RuleFor(x => x.RouteId)
.NotEmpty().WithMessage("RouteId is required")
.NotEqual(Guid.Empty).WithMessage("RouteId must be a valid GUID");
RuleFor(x => x.SubId)
.NotEmpty().WithMessage("SubId is required")
.NotEqual(Guid.Empty).WithMessage("SubId must be a valid GUID");
}
}
This approach improves readability and eliminates repetitive validation code inside the main handler.
Preprocessing Example
public async Task PreProcessAsync(
IPreProcessorContext<MyRequest> ctx, CancellationToken c)
{
if (ctx.Request is null)
{
await ctx.HttpContext.Response.SendAsync(
new FastEndpoints.ProblemDetails(ctx.ValidationFailures),
StatusCodes.Status400BadRequest, cancellation: c);
return;
}
var service = ctx.HttpContext.RequestServices
.GetRequiredService<ExternalConnectionService>();
var connection = await service.GetAsync(
ctx.Request.CompanyId,
Guid.Parse(ctx.Request.SubId));
if (connection is null)
{
await ctx.HttpContext.Response.SendAsync(
new FastEndpoints.ProblemDetails([
new() { ErrorMessage = "No External Connection Found",
PropertyName = "subId" }
]),
StatusCodes.Status400BadRequest, cancellation: c);
return;
}
ctx.Request.ExternalConnection = connection;
}
This flexibility is key for handling custom scenarios without compromising structure or clarity.
Conclusion
Migrating from Azure Functions to Minimal API can bring significant benefits, especially for business-critical applications requiring better structure and performance. By leveraging FastEndpoints, you can ensure a smooth transition while maintaining clarity and scalability in your code.
Have you migrated an application from Azure Functions to Minimal API? Share your experience in the comments or try out FastEndpoints to see how it simplifies API development.
Top comments (0)