Creating a Minimal API With ASP.NET Core That Interacts With AWS DynamoDB

In this blog post, we will guide you through the process of creating a simple .NET 7 API that interacts with DynamoDB. Additionally, we will integrate our SlackBot console application, which we introduced in a previous blog post How to Create a Custom Slack Bot with .NET 7.

This is the second post in the series

💻 How to Create a Custom Slack Bot with .NET 7

🚀 Creating a Minimal API With ASP.NET Core That Interacts With AWS DynamoDB

🔧 Creating and Hosting An Application on AWS Elastic Container Service (ECS)

🚢 Continuous Deployment with GitHub Actions to AWS Elastic Container Service (ECS)

Before we dive into the details, it’s important to note that we could have created everything within the SlackBot console application. If you prefer that approach, feel free to do so. However, in this post, we are creating a separate Web API to allow for future integration with front-end applications, rather than solely relying on the SlackBot console application.

In this post, we are going to cover creating our ASP.NET Core API first. We will then add a ping endpoint to ensure our Web API is working as expected. Next, we will create an IAM role in the AWS Management Console, create a DynamoDB table in the AWS Management Console, add the AWSDynamoDB SDK and finally the code needed to call our DynamoDB table in AWS.


Don’t miss out on our upcoming posts! Subscribe now to stay in the loop and be the first to receive our latest content.


Creating an ASP.NET Core API

First, ensure that you have the .NET SDK installed as it is required to create our ASP.NET Core API using our command line interface. If you have not yet installed the SDK, head over to Microsoft’s download page.

Inside your command line interface (I’m using PowerShell), navigate to the directory where you want your API to be created. Then, type the following command:

dotnet new web -o SlackBot.WebAPI

Next, open your newly created project in your preferred IDE (I’m using Rider).

Minimal API

Minimal APIs are designed to build HTTP APIs with few dependencies. They are well-suited for microservices and applications that aim to have a lightweight structure by including only essential files, features, and dependencies in ASP.NET Core.

Rick Anderson and Tom Dykstra have written a good tutorial on creating a minimal API TodoList with ASP.NET Core. You can view that here.

Now let’s add a ping endpoint to our Web API.

Adding a Ping Endpoint

The first endpoint we want to add is a ping endpoint. This is a simple endpoint that, when hit, will return an OK 200 status code. It is used to ensure that our API runs and is able to receive requests. This also helps demonstrate how we are going to set up our endpoints using the minimal style.

Inside our application, let’s examine the code in our Program.cs file.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

As you can see, it’s very lightweight. Starting with .NET 6, Microsoft has removed the need to include a Startup.cs file and other boilerplate code inside the Program.cs file. The aim is to reduce any extra methods, lines of code, and extra braces (i.e., {}) from the file to reduce the amount of unnecessary details.

We are going to delete line 4 (the example endpoint) and then add our own.

Inside our Program.cs file, after the builder.Build(); line, we are going to be using the TypedResults API. For more information on the standard Results vs. TypedResults, see “How to create responses in Minimal API apps“. In short, using TypedResults instead of Results offers several benefits, such as improved testability and automatic inclusion of response type information for OpenAPI documentation.

After the builder.Build(); line, add the following code to create a ping endpoint:

app.MapGet("/ping", Ping);

Then, after the app.Run(); line, add the following code:

static IResult Ping()
{
    return TypedResults.Ok("Success");
}

This code adds a new endpoint at the /ping route, which returns a response with the message “Ping successful” when it’s called.

Now, let’s run our API and test the ping endpoint. In your command-line tool, navigate to the project directory and run the following command:

dotnet run

Alternatively run the application in your IDE.

Once the API is running, open your browser or a tool like Postman and make a GET request to https://localhost:44335/ping. You should receive a response with the message “Success” and a status code of 200.

With the ping endpoint set up, we can move on creating our DynamoDB table and IAM user.

Creating a DynamoDB Table in the AWS Management Console

To create a DynamoDB table in the AWS Management Console, follow these steps:

1. Create an AWS account if you don’t have one already. You can sign up for a free account here.

This page also explains about AWS free tier solutions. This means that for DynamoDB we get the following for free

2. Log into the AWS Management Console using your account credentials. The console URL is https://console.aws.amazon.com.

3. In the search box at the top of the page, type “DynamoDB” and select “DynamoDB” from the search results.

4. Click on “Create table” to create a new table.

5. Enter a table name, such as “ToDoList”, in the “Table name” field.

6. Enter a partition key in the “Partition key” field. For example, enter “Id”. In DynamoDB, the partition key value plays a crucial role by serving as input to an internal hash function. This function generates a specific output that determines the partition where an item will be stored within DynamoDB’s internal storage. Essentially, items sharing the same partition key value are grouped together and stored in a particular partition. Moreover, these items are arranged in a sorted order based on their respective sort key values.

7. Set any additional configuration options as needed.

8. Scroll down to the bottom of the page and click on “Create table” to create the DynamoDB table.

Once the table is created, you can use it to store and retrieve data.

Next, we want to create an IAM User to gain an Access and Secret Key that we will use to connect to our AWS account from our API.

Create an IAM User with an Access and Secret key

1. In the AWS Management Console, navigate to IAM and select “Users” from the left-hand menu.

2. Click on “Add users” and provide a suitable user name.

3. Specify user details according to your requirements.

4. On the “Set permissions” page, create a group. This will allow better management of user permissions by creating a group that we could then assign other people to. Give the group a meaningful name, such as “SlackBotDynamoDB”.

5. Search for “AmazonDynamoDbFullAccess” policy and attach it to the group.

6. Review the user details and click “Create User”.

7. Once the user is created, select it under “Users” in the main IAM page.

8. Go to the “Security Credentials” tab, click on “Create access key”, and follow the instructions to generate an access key and secret key. Remember to store them securely.

💡 AWS will give you suggestions on how to better manage access to AWS services. For now we are going to stick with our Access and Security Keys for testing purposes. Select Local code and ensure you check the checkbox saying you understand the recommendation and then next.

You will be presented with the following page ‘Retrieve access keys’ :

🚩 This will be the only time you can see your Secret Keys, so make sure you store them in a safe location.

Setting up AWS Credentials

To configure your AWS credentials to interact with your DynamoDB table, you can install the AWS CLI. If you haven’t installed it yet, refer to the AWS CLI Installation Guide.

To verify if you have the AWS CLI installed correctly, run the following command:

aws --version

Then run the following command to setup your credentials:

aws configure

You will be prompted to enter your access key and secret key, which you saved from the previous step. Additionally, provide the region where your DynamoDB table is located. For example, if your table is in the Sydney region (ap-southeast-2), enter “ap-southeast-2” as the default region. Leave the default output format empty and press enter to finish the AWS key setup. This process will create two files: a credentials file (containing access and secret key) and a config file (containing the specified region).You can find these files in the following location:

%userprofile%/.aws 

Adding the AWS SDK and DynamoDB Code

To incorporate the necessary functionality for DynamoDB and enable dependency injection in our API project, we will add two packages: AWSSDK.DynamoDBv2 and AWSSDK.Extensions.NETCore.Setup.

This can be done using the dotnet CLI. Open your command line interface and execute the following commands:

dotnet add package AWSSDK.DynamoDBv2
dotnet add package AWSSDK.Extensions.NETCore.Setup

These commands will install the required packages: AWSSDK.DynamoDBv2 provides the methods to interact with DynamoDB, and AWSSDK.Extensions.NETCore.Setup allows for dependency injection of our AWS service client.

After successfully installing the AWSSDK.DynamoDBv2 package, we need to perform a few additional steps

Register IAmazonDynamoDb Interface and Load Credentials File

Returning to our SlackBot.WebAPI application, we need to register the GetAWSOptions method from Amazon.Extensions.NETCore and the IAmazonDynamoDB interface from AWSSDK.DynamoDBv2.

In the Program.cs class, add the following code snippet after the WebApplication.CreateBuilder() call but before the builder.Build():

builder.Configuration.GetAWSOptions();
builder.Services.AddAWSService<IAmazonDynamoDB>();

The GetAWSOptions method retrieves the credentials and region from the credentials and config files we created earlier. The AddAWSService registers the IAmazonDynamoDB interface, enabling us to dependency injection the IAmazonDynamoDB in other classes.

Our Program.cs class should look like the following.

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.GetAWSOptions();
builder.Services.AddAWSService<IAmazonDynamoDB>();

var app = builder.Build();

app.MapGet("/ping", Ping);

app.Run();

static IResult Ping()
{
    return TypedResults.Ok("Success");
}

Add Items to DynamoDB

To add items to DynamoDB using the Object Persistence Model, follow these steps

💡 DynamoDB SDK for .NET offers three models that we can use. Low Level API, Document Model and Object Persistence Model. We are going to use the Object Persistence Model for our project. If you want to learn more about the different models have a look at the following AWS Site.

1. Create a new class named DynamoDbRepository in a folder named Repositories. In this class, initialize the DynamoDBContext and inject the IAmazonDynamoDB interface:

private readonly DynamoDBContext _context;

public DynamoDbRepository(IAmazonDynamoDB dynamoDbClient)
{
    _context = new DynamoDBContext(dynamoDbClient);
}

2. Create a model class named ToDoRequest with properties for Id, Name, IsComplete, and Created. Decorate the class with DynamoDB attributes:

[DynamoDBTable("ToDoList")]
public class ToDoRequest
{
    [DynamoDBHashKey]
    public string Id { get; set; }
    public string Name { get; set; }
    public bool IsComplete { get; set; }
    public DateTime Created { get; set; }
}

3. Create a ToDoResponse class with properties matching the response format:

public class ToDoResponse
{
    public string Name { get; set; }
    public bool IsComplete { get; set; }
    public DateTime Created { get; set; }
}

4. Inside the DynamoDbRepository class, add a method named AddItem that takes a ToDoRequest parameter and returns a Task<ToDoResponse>:

public async Task<ToDoResponse> AddItem(ToDoRequest toDoRequest)
{
    var uuid = Guid.NewGuid().ToString();
    var nzDateTime = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, TimeZoneInfo.FindSystemTimeZoneById("New Zealand Standard Time"));

    var request = new ToDoRequest
    {
        Id = uuid,
        Name = toDoRequest.Name,
        IsComplete = toDoRequest.IsComplete,
        Created = nzDateTime
    };

    await _context.SaveAsync(request);

    return new ToDoResponse
    {
        Name = request.Name,
        IsComplete = request.IsComplete,
        Created = request.Created
    };
}

5. In the Program.cs file, register the DynamoDbRepository as a singleton:

builder.Services.AddSingleton<DynamoDbRepository>();

6. Add a route to access the AddItems endpoint:

app.MapPost("/todoitems/", AddToDoItem);

7. Create the AddToDoItem method that takes a ToDoRequest and DynamoDbRepository as parameters. Inside the method, call the AddItem method of the DynamoDbRepository:

static async Task<IResult> AddToDoItem([FromBody] ToDoRequest todo, DynamoDbRepository dynamoDbRepository)
{
    var response = await dynamoDbRepository.AddItem(todo);

    return TypedResults.Created($"/todoitems/{todo.Id}", response);
}

This method calls the AddItem method and returns the response with a status of Created.

With these steps, you have added the functionality to add items to DynamoDB using the Object Persistence Model.

Our program.cs class looks like the following

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.GetAWSOptions();
builder.Services.AddAWSService<IAmazonDynamoDB>();
builder.Services.AddSingleton<DynamoDbRepository>();

var app = builder.Build();

app.MapGet("/ping", Ping);
app.MapPost("/todoitems/", AddToDoItem);

app.Run();

static IResult Ping()
{
    return TypedResults.Ok("Success");
}

static async Task<IResult> AddToDoItem([FromBody] ToDoRequest todo, DynamoDbRepository dynamoDbRepository)
{
    var response = await dynamoDbRepository.AddItem(todo);

    return TypedResults.Created($"/todoitems/{todo.Id}", response);
}

Get Items from DynamoDB

To retrieve all items from DynamoDB, follow these steps:

1. In the DynamoDbRepository class, add a method named GetAllItems that returns a Task<IEnumerable<ToDoResponse>>:

public async Task<IEnumerable<ToDoResponse>> GetAllItems()
{
    var scanConfig = new ScanOperationConfig();
    var searchResults = _context.ScanAsync<ToDoRequest>(scanConfig);
    var response = await searchResults.GetNextSetAsync();

    return response.Select(item => new ToDoResponse
    {
        Name = item.Name,
        IsComplete = item.IsComplete,
        Created = item.Created
    });
}

This method uses the ScanAsync method provided by the IAmazonDynamoDB interface to perform a scan operation on the DynamoDB table. The response is then mapped to ToDoResponse objects to include only the desired information.

2. In the Program.cs file, add a route to access the “GetAllItems” endpoint:

app.MapGet("/todoitems", GetToDoItems);

3. Create the GetToDoItems static method that takes DynamoDbRepository as a parameter. Inside the method, call the GetAllItems method of the DynamoDbRepository:

static async Task<IResult> GetToDoItems(DynamoDbRepository dynamoDbRepository)
{
    var response = await dynamoDbRepository.GetAllItems();

    return Results.Ok(response);
}

This method calls the GetAllItems method and returns the response with a status of OK (200).

With these steps, you have added the functionality to retrieve all items from DynamoDB using the Object Persistence Model.

Testing in Postman

To test the routes using Postman, follow these steps:

1. Run your application, and you will see two URLs printed in the application console window. These URLs correspond to the endpoints you have defined.

2. Open Postman and create a new request.

3. For testing the POST endpoint to add items, use the following details:

  • Method: POST
  • URL: https://localhost:44335/todoitems
  • In the body tab, select “Raw” and set the body format to JSON.
  • Add the request properties inside a ToDoRequest object, for example:
{
  "Name": "Finish AWS Blog",
  "IsComplete": "false"
}
  • Send the request and observe the response, which should include a ToDoResponse object with the provided name, completion status, and created date.

4. For testing the GET endpoint to retrieve all items, use the following details:

  • Method: GET
  • URL: https://localhost:44335/todoitems
  • Send the request and observe the response, which should include an array of ToDoResponse objects representing the items stored in the DynamoDB table.

By testing these endpoints in Postman, you can verify that the routes are functioning correctly and interacting with the DynamoDB table as expected.

Connecting our SlackBot console application to the SlackBot API

The next stage is to create an http request from our SlackBot console application to call off to our SlackBot API project. To allow us to add and get our To Do items.

To hook up your SlackBot console application to your SlackBot API, follow these steps:

1. Install the Microsoft.Extensions.Http package by running the following command:

dotnet add Microsoft.Extensions.Http

This is going to allow us to register Microsoft’s AddHttpClient shown in step 5.

2. Create a new class named SlackBotApiService with the following code:

public class SlackBotApiService
{
    private readonly HttpClient _httpClient;

    public SlackBotApiService(HttpClient httpClient)
    {
        _httpClient = httpClient;
        _httpClient.BaseAddress = new Uri("https://localhost:44335/");
    }

    public async Task<IEnumerable<ToDoResponse?>> GetToDoItems() => await _httpClient.GetFromJsonAsync<IEnumerable<ToDoResponse>>("todoitems");
}

We have implemented a Typed Client in our solution. If you want to delve deeper into HTTP requests and explore the different styles you can use in ASP.NET Core, I recommend consulting the comprehensive documentation provided by Microsoft. The documentation covers various approaches to making HTTP requests, including the usage of the HttpClient class, the HttpRequestMessage class, and the IHttpClientFactory. It provides detailed information and valuable insights to enhance your understanding. You can access the documentation by visiting HTTP requests in ASP.NET Core.

3. Create a class named ToDoResponse to represent the response model from the API:

public class ToDoResponse
{
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
    public DateTime Created { get; set; }
}

This is the same response model we used in our SlackBot.WebApi project.

4. Create a class named GetToDoItemsHandler that implements the IEventHandler<MessageEvent> interface. This handler will handle the “get todo items” command from Slack and retrieve the to-do items from the API:

public class GetToDoItemsHandler : IEventHandler<MessageEvent>
{
    private const string Trigger = "get todo items";

    private readonly ISlackApiClient _slack;
    private readonly SlackBotApiService _slackBotApiService;

    public GetToDoItemsHandler(ISlackApiClient slack, SlackBotApiService slackBotApiService)
    {
        _slack = slack;
        _slackBotApiService = slackBotApiService;
    }

    public async Task Handle(MessageEvent slackEvent)
    {
        if (slackEvent.Text?.Equals(Trigger, StringComparison.OrdinalIgnoreCase) == true)
        {
            var items = await _slackBotApiService.GetToDoItems();

            var members = new StringBuilder();
            foreach (var item in items)
            {
                members.AppendLine($":red_circle: {item.Name}");
                members.AppendLine($":white_check_mark: Completed: {item.IsComplete}");
                members.AppendLine($":date: Created: {item.Created}");
                members.AppendLine();
            }

            await _slack.Chat.PostMessage(new Message { 
                Text = members.ToString(), 
                Channel = slackEvent.Channel });
        }
    }
}

In this class, there are a few important components at play. First, we set the trigger, which is the specific phrase the bot listens for in Slack. When the trigger phrase “get todo items” is detected, the rest of the code in this class will execute.

We utilize dependency injection to inject the HttpClient and SlackBotApiService into our class. This allows us to make HTTP requests to our SlackBot.WebApi.

Within the Handle method, we make a call to our SlackBot.WebApi using the SlackBotApiService. Once we receive the response from the API, we use the StringBuilder method to iterate through the results and format them into separate lines. To enhance the appearance in Slack, we include emojis such as :red_circle: to make the content visually appealing.

Finally, we post the formatted message to the channel in Slack, providing the user with the requested to-do items in an organized manner.

5. Add the following code to your Program.cs class below the var services = new ServiceCollection(); call:

services.AddHttpClient<SlackBotApiService>();

6. Register the GetToDoItemsHandler handler in your Program.cs class:

.RegisterEventHandler<MessageEvent, GetToDoItemsHandler>()

Now that we have the logic to get our to do items, let’s now add a way to add items.

Create a new class named AddToDoItemHandler that implements the IEventHandler<MessageEvent> interface. This handler will handle the “add todo” command from Slack and add a new to-do item to the API:

public class AddToDoItemHandler : IEventHandler<MessageEvent>
{
    private const string Trigger = "add todo";

    private readonly ISlackApiClient _slack;
    private readonly SlackBotApiService _slackBotApiService;

    public AddToDoItemHandler(ISlackApiClient slack, SlackBotApiService slackBotApiService)
    {
        _slack = slack;
        _slackBotApiService = slackBotApiService;
    }

    public async Task Handle(MessageEvent slackEvent)
    {
        if (slackEvent.Text?.Contains(Trigger, StringComparison.OrdinalIgnoreCase) == true)
        {
            var toDoItemName = slackEvent.Text.Split(',').Skip(1).FirstOrDefault();
            var isComplete = Convert.ToBoolean(slackEvent.Text.Split(',').Skip(2).FirstOrDefault());

            var request = new ToDoRequest { Name = toDoItemName, IsComplete = isComplete };
            await _slackBotApiService.PostToDoItems(request);

            await _slack.Chat.PostMessage(new Message { 
                Text = $"{toDoItemName} has been added to your to do list", 
                Channel = slackEvent.Channel });
        }
    }
}

7. Add the following code to the SlackBotApiService class to handle the POST request for adding a to-do item:

    public async Task PostToDoItems(ToDoRequest todoItem)
    {
        var todoItemJson = new StringContent(JsonSerializer.Serialize(todoItem), Encoding.UTF8, MediaTypeNames.Application.Json);

        await _httpClient.PostAsync("todoitems", todoItemJson);
    }
}

8. Create a class named ToDoRequest to represent the request model for adding a to-do item:

public class ToDoRequest
{
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

9. Register the AddToDoItemHandler handler in your Program.cs class:

.RegisterEventHandler<MessageEvent, AddToDoItemHandler>()

10. In your Program.cs class, add the following code to configure the SlackBotApiService as an HttpClient service:

services.AddHttpClient<SlackBotApiService>();

With these changes, your SlackBot console application will be able to interact with the SlackBot API to add and retrieve to-do items from Slack.

Testing In Slack

Now that we have all the code written. We can now run both applications to check how it works.

Ensure you have both the SlackBot.WebAPI and SlackBot console application running.

Inside our Slack Workspace that we created in our previous blog. We can type the following commands

get todo items

Provided that you have items in your DynamoDb database, you should get a response back with those items.

We can also add items, but saying

add todo, Testing Adding Item, False 
💡 I have used the comma “,” to allow me to separate out the words, first the name of my item and second if the task has been completed. You might want to look into a better way to handle this, but for now, this is what we will go with. 

Once you have your item added, try using get todo list again and you should see your new to do item.


If you liked this post make sure to subscribe

Final Thoughts

There are numerous ways to enhance and ensure the robustness of our SlackBot. For instance, instead of relying on text input for adding items, we can employ a model to input To-Do items. This approach would provide clarity and eliminate the need for commas to separate each property. Additionally, we can further expand the code to enable task completion, display only completed or non-completed tasks, and implement stronger null exception handling.

This blog has provided a concise overview of building an ASP.NET Core API that interacts with Slack and AWS DynamoDB.

Armed with the knowledge gained here, you are now equipped to develop your own powerful SlackBot applications tailored to your specific requirements. Happy coding!

3 thoughts on “Creating a Minimal API With ASP.NET Core That Interacts With AWS DynamoDB

Leave a comment