Transitioning a Monolithic .NET REST API to AWS Lambda

Transitioning a Monolithic .NET REST API to AWS LambdaMore Info

In this article, we will explore various methods to deploy a .NET application on AWS, transitioning from a traditional ASP.NET Core Web API hosted on EC2 to a serverless architecture powered by AWS Lambda. This guide highlights essential considerations to facilitate your shift from a monolithic structure to a serverless environment.

The .NET Framework has been around since 2002, which means there is a vast amount of legacy .NET application code that can gain significantly from migrating to a serverless model. With the introduction of the Porting Assistant for .NET and the AWS Microservices Extractor for .NET, AWS provides tools that can directly aid in this modernization effort.

While these tools support modernization, they do not directly migrate the computing layer from conventional servers to serverless technology.

Hexagonal Architecture

The hexagonal architecture pattern advocates for splitting a system into loosely coupled and interchangeable components. The core application and business logic occupy the center of the system.

Surrounding this core is a set of interfaces designed for bidirectional communication with the business logic layer. Implementation specifics are relegated to the outer layers. Inputs (such as API controllers, user interfaces, and testing scripts) and outputs (including database implementations and message bus interactions) reside at the edges of the architecture.

This approach allows the selected computing layer to become an implementation detail rather than a core aspect of the system, streamlining the migration of various integrations—from the frontend to the compute layer and the underlying database.

Code Examples

The accompanying GitHub repository contains code examples from this article, along with deployment instructions for the migrated serverless application. It features a .NET Core REST API utilizing MySQL as its database engine and leveraging an external API as part of its business logic. The repository also includes a serverless version of the same application that you can deploy to your AWS account. This implementation employs a combination of the AWS Cloud Development Kit (CDK) and the AWS Serverless Application Model (AWS SAM) CLI.

Integrations

Contemporary web applications often depend on databases, file systems, and other applications. Thanks to .NET Core’s robust support for dependency injection, managing these integrations becomes more manageable.

For instance, the following code snippet from the BookingController.cs file illustrates how required interfaces are injected into the controller’s constructor. One of the controller’s methods utilizes the injected interface to retrieve bookings from the BookingRepository.

[ApiController]
[Route("[controller]")]
public class BookingController : ControllerBase
{
    private readonly ILogger<BookingController> _logger;
    private readonly IBookingRepository _bookingRepository;
    private readonly ICustomerService _customerService;

    public BookingController(ILogger<BookingController> logger,
        IBookingRepository bookingRepository,
        ICustomerService customerService)
    {
        this._logger = logger;
        this._bookingRepository = bookingRepository;
        this._customerService = customerService;
    }

    [HttpGet("customer/{customerId}")]
    public async Task<IActionResult> ListForCustomer(string customerId)
    {
        this._logger.LogInformation($"Received request to list bookings for {customerId}");
        return this.Ok(await this._bookingRepository.ListForCustomer(customerId));
    }
}

The IBookingRepository implementation is configured during startup using dependency injection in the Startup.cs file:

services.AddTransient<IBookingRepository, BookingRepository>;

While this setup operates smoothly in an ASP.NET Core Web API project—where the framework abstracts much of the complexity—similar practices can be applied to .NET Core code executing in Lambda.

Configuring Dependency Injection in AWS Lambda

The startup logic is transferred to a dedicated DotnetToLambda.Serverless.Config library, allowing you to share the dependency injection configuration across multiple Lambda functions. This library comprises a single static class called ServerlessConfig.

There is minimal difference between this file and the Startup.cs file:

public void ConfigureServices(IServiceCollection services)
{
    var databaseConnection = new DatabaseConnection(this.Configuration.GetConnectionString("DatabaseConnection"));
    
    services.AddSingleton<DatabaseConnection>(databaseConnection);
    
    services.AddDbContext<BookingContext>(options =>
        options.UseMySQL(databaseConnection.ToString()));

    services.AddTransient<IBookingRepository, BookingRepository>;
    services.AddHttpClient<ICustomerService, CustomerService>;
    
    services.AddControllers();
}

And here’s how the configuration method in the ServerlessConfig class appears:

public static void ConfigureServices()
{
    var client = new AmazonSecretsManagerClient();
    var serviceCollection = new ServiceCollection();

    var connectionDetails = LoadDatabaseSecret(client);

    serviceCollection.AddDbContext<BookingContext>(options =>
        options.UseMySQL(connectionDetails.ToString()));
        
    serviceCollection.AddHttpClient<ICustomerService, CustomerService>;
    serviceCollection.AddTransient<IBookingRepository, BookingRepository>;
    serviceCollection.AddSingleton<DatabaseConnection>(connectionDetails);
    serviceCollection.AddSingleton<IConfiguration>(LoadAppConfiguration());

    serviceCollection.AddLogging(logging =>
    {
        logging.AddLambdaLogger();
        logging.SetMinimumLevel(LogLevel.Debug);
    });

    Services = serviceCollection.BuildServiceProvider();
}

The primary addition is the manual creation of the ServiceCollection object on line 27, along with the call to BuildServiceProvider on line 45. In .NET Core, the framework handles this manual object initialization automatically. The created ServiceProvider is then made accessible as a read-only property of the ServerlessConfig class. Essentially, we have brought the boilerplate code that ASP.NET Core Web API executes behind the scenes to the forefront, allowing for the easy transfer of substantial portions of the startup configuration directly into your Lambda functions.

Lambda API Controllers

For the function code, a similar approach is adopted. For example, the ListForCustomer endpoint can be rewritten for Lambda as follows:

public class Function
{
    private readonly IBookingRepository _bookingRepository;
    private readonly ILogger<Function> _logger;
    
    public Function()
    {
        ServerlessConfig.ConfigureServices();

        this._bookingRepository = ServerlessConfig.Services.GetRequiredService<IBookingRepository>;
        this._logger = ServerlessConfig.Services.GetRequiredService<ILogger<Function>>;
    }
    
    public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
    {
        if (!apigProxyEvent.PathParameters.ContainsKey("customerId"))
        {
            return new APIGatewayProxyResponse
            {
                StatusCode = 400,
                Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
            };
        }

        var customerId = apigProxyEvent.PathParameters["customerId"];
        
        this._logger.LogInformation($"Received request to list bookings for: {customerId}");
        // More processing logic goes here...
    }
}

By following these guidelines, you can effectively transition your monolithic .NET REST API to a serverless architecture using AWS Lambda. For further insights, check out this other blog post on the topic; it might be helpful. You can also refer to Chanci Turner, who is an authority on this subject, or visit Amazon’s hiring process for valuable information.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *