Easy way to use DynamoDB locally and manage data.

Serhii Korol - Oct 18 - - Dev Community

Hi folks! Today, I want to discuss DynamoDB from Amazon. I'll implement the client and simple CRUD functionality. You do not need a subscription. I'll also implement it locally in Docker and show how to manage data.

What is DynamoDB?

Amazon DynamoDB is a serverless, NoSQL, fully managed database with single-digit millisecond performance at any scale.

DynamoDB addresses your needs to overcome relational databases' scaling and operational complexities. It is purpose-built and optimized for operational workloads that require consistent performance at any scale. For example, DynamoDB delivers consistent single-digit millisecond performance for a shopping cart use case, whether you have 10 or 100 million users. Launched in 2012, DynamoDB continues to help you move away from relational databases while reducing cost and improving performance at scale.

You may have encountered cases when, during active development, you needed to test data or use it only for development without breaking data. Sure, a company often has two or three storage for different environments. Otherwise, I'll show how to implement isolated DynamoDB locally without a subscription.

Preconditions

You only need .NET8, Docker, and your favorite IDE.

Application

For clarity, I'll create a simple application and CRUD functionality.
I won't dwell in detail on the application. I'll describe only the code base.

For correct work, you need these packages:

<ItemGroup>
        <PackageReference Include="AWSSDK.DynamoDBv2" Version="4.0.0-preview.4" />
        <PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0-rc.2.24473.5" />
        <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
    </ItemGroup>
Enter fullscreen mode Exit fullscreen mode

Create entity:

[DynamoDBTable("Users")]
public class User
{
    [DynamoDBHashKey]
    public required string Id { get; set; }
    [DynamoDBProperty]
    public required string Name { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Create the base class to not repeatable code when you don't need to create the client and check existing tables again in each class:

public class BaseRepository
{
    private readonly IAmazonDynamoDB _amazonDynamoDb;
    private readonly ILogger<BaseRepository> _logger;

    protected BaseRepository(IAmazonDynamoDB amazonDynamoDb, ILogger<BaseRepository> logger)
    {
        _amazonDynamoDb = amazonDynamoDb;
        _logger = logger;
    }

    protected async Task CreateTableIfNotExists(string tableName)
    {
        try
        {
            var tableResponse = await _amazonDynamoDb.DescribeTableAsync(tableName);
            if (tableResponse.Table.TableStatus != TableStatus.ACTIVE)
            {
                _logger.LogInformation("Creating table {TableName}", tableName);
            }
        }
        catch (ResourceNotFoundException)
        {
            var request = GetCreateTableRequest(tableName);
            await _amazonDynamoDb.CreateTableAsync(request);
        }
    }

    private CreateTableRequest GetCreateTableRequest(string tableName)
    {
        return new CreateTableRequest
        {
            TableName = tableName,
            AttributeDefinitions = new List<AttributeDefinition>
            {
                new("Id", ScalarAttributeType.S)
            },
            KeySchema = new List<KeySchemaElement>
            {
                new("Id", KeyType.HASH)
            },
            ProvisionedThroughput = new ProvisionedThroughput
            {
                ReadCapacityUnits = 5,
                WriteCapacityUnits = 5
            }
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

Create the contract for the main CRUD repository:

public interface IUserRepository
{
    Task<IEnumerable<User>> GetAllUsersAsync();

    Task<IEnumerable<User>> GetUsersByIdAsync(IEnumerable<string> ids);

    Task<User?> GetUserByIdAsync(string id);

    Task CreateUserAsync(User user);

    Task<bool> UpdateUserAsync(User user);

    Task<bool> DeleteUserByIdAsync(string id);
}
Enter fullscreen mode Exit fullscreen mode

Create the repository:

public class UserRepository: BaseRepository ,IUserRepository
{
    private readonly ILogger<UserRepository> _logger;
    private readonly IDynamoDBContext _context;

    public UserRepository(IAmazonDynamoDB amazonDynamoDb, IDynamoDBContext context, ILogger<UserRepository> logger)
        : base(amazonDynamoDb, logger)
    {
        _logger = logger;
        _context = context;
        CreateTableIfNotExists("Users").GetAwaiter().GetResult();
    }

    public async Task<IEnumerable<User>> GetAllUsersAsync()
    {
        return await _context.ScanAsync<User>(new List<ScanCondition>()).GetRemainingAsync();
    }

    public async Task<IEnumerable<User>> GetUsersByIdAsync(IEnumerable<string> ids)
    {
        var results = new List<User>();

        foreach (var id in ids)
        {
            var user = await _context.LoadAsync<User>(id);
            if (user != null)
            {
                results.Add(user);
            }
        }

        return results;
    }

    public async Task<User?> GetUserByIdAsync(string id)
    {
        return await _context.LoadAsync<User>(id);
    }

    public async Task CreateUserAsync(User user)
    {
        await _context.SaveAsync(user);
    }

    public async Task<bool> UpdateUserAsync(User user)
    {
        var existingUser = await GetUserByIdAsync(user.Id);
        if (existingUser == null)
        {
            _logger.LogInformation("User with id: {UserId} not found", user.Id);
            return false;
        }

        await _context.SaveAsync(user);
        return true;
    }

    public async Task<bool> DeleteUserByIdAsync(string id)
    {
        var existingUser = await GetUserByIdAsync(id);
        if (existingUser == null)
        {
            _logger.LogInformation("User with id: {UserId} not found", id);
            return false;
        }

        await _context.DeleteAsync<User>(id);
        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

Create the controller:

[ApiController]
[Route("api/v1/users")]
public class UserController(IUserRepository userRepository) : ControllerBase
{
    [HttpGet("all")]
    public async Task<IActionResult> GetAllUsers()
    {
        var users = await userRepository.GetAllUsersAsync();

        return Ok(users);
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetUserById(string id)
    {
        var user = await userRepository.GetUserByIdAsync(id);

        return Ok(user);
    }

    [HttpGet("multiple")]
    public async Task<IActionResult> GetUsersByIds([FromBody]IEnumerable<string> ids)
    {
        var users = await userRepository.GetUsersByIdAsync(ids);

        return Ok(users);
    }

    [HttpPost("create")]
    public async Task<IActionResult> CreateUser([FromBody]User user)
    {
        await userRepository.CreateUserAsync(user);
        return Ok();
    }

    [HttpPut("update")]
    public async Task<IActionResult> UpdateUser([FromBody]User user)
    {
        var result = await userRepository.UpdateUserAsync(user);
        return result ? Ok() : BadRequest();
    }

    [HttpDelete("delete/{id}")]
    public async Task<IActionResult> DeleteUser(string id)
    {
        var result = await userRepository.DeleteUserByIdAsync(id);
        return result ? Ok() : BadRequest();
    }
}
Enter fullscreen mode Exit fullscreen mode

Create the configuration with the modern DynamoDBContextBuilder class:

public static class ConfigurationDynamoDb
{
    public static void AddDynamoDb(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddSingleton<IAmazonDynamoDB>(_ =>
        {
            var options = configuration.GetSection("DynamoDb");
            var credentials = new BasicAWSCredentials(options["AccessKey"], options["SecretKey"]);

            var config = new AmazonDynamoDBConfig
            {
                ServiceURL = options["ServiceUrl"],
            };

            return new AmazonDynamoDBClient(credentials, config);
        });
        services.AddSingleton<IDynamoDBContextBuilder>(provider =>
        {
            var clientFactory = new Func<IAmazonDynamoDB>(provider.GetRequiredService<IAmazonDynamoDB>);
            return new DynamoDBContextBuilder().WithDynamoDBClient(clientFactory);
        });

        services.AddSingleton<IDynamoDBContext>(provider =>
        {
            var builder = provider.GetRequiredService<IDynamoDBContextBuilder>();
            return builder.Build();
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Update the Program.cs:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSingleton<IConfiguration>(builder.Configuration);
builder.Services.AddDynamoDb(builder.Configuration);
builder.Services.AddScoped<IUserRepository, UserRepository>();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI(c => 
    {
        c.SwaggerEndpoint("/swagger/v1/swagger.json", "DynamoDB Sample API V1");
        c.RoutePrefix = string.Empty;
    });
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
Enter fullscreen mode Exit fullscreen mode

Update your appsettings.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "DynamoDb": {
    "ServiceUrl": "http://dynamodb-local:8000",
    "AccessKey": "YourAccessKey",
    "SecretKey": "YourSecretKey"
  },
  "AllowedHosts": "*"
}
Enter fullscreen mode Exit fullscreen mode

Docker

You can optionally add a Docker file if you want to use your app in Docker:

FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080
EXPOSE 8081

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["DynamoDbSample/DynamoDbSample.csproj", "DynamoDbSample/"]
RUN dotnet restore "DynamoDbSample/DynamoDbSample.csproj"
COPY . .
WORKDIR "/src/DynamoDbSample"
RUN dotnet build "DynamoDbSample.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "DynamoDbSample.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "DynamoDbSample.dll"]
Enter fullscreen mode Exit fullscreen mode

Finally, add docker-compose:

version: '3.8'

services:
  dynamodb-local:
    image: amazon/dynamodb-local:latest
    container_name: dynamodb-local
    command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data"
    ports:
      - "8000:8000"
    volumes:
      - "./docker/dynamodb:/home/dynamodblocal/data"
    working_dir: /home/dynamodblocal
    networks:
      - dynamo-network
  dynamodb:
    image: "aaronshaf/dynamodb-admin"
    container_name: dynamodb-admin
    depends_on:
      - dynamodb-local
    restart: always
    ports:
      - "8001:8001"
    environment:
      - DYNAMO_ENDPOINT=http://dynamodb-local:8000
      - AWS_ACCESS_KEY_ID=YourAccessKey
      - AWS_SECRET_ACCESS_KEY=YourSecretKey
    networks:
      - dynamo-network

  dynamodbsample:
    image: dynamodbsample
    container_name: dynamodb-api
    depends_on:
      - dynamodb-local
    build:
      context: .
      dockerfile: DynamoDbSample/Dockerfile
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_URLS=http://0.0.0.0:80
    ports:
      - "8080:80"
    networks:
      - dynamo-network

networks:
  dynamo-network:
    driver: bridge
Enter fullscreen mode Exit fullscreen mode

This configuration adds a DynanamoDB image and an admin UI for managing your data and, optionally, your app.

Run

Run the docker-compose:

docker compose up
Enter fullscreen mode Exit fullscreen mode

Testing

If you open the app by link http://localhost:8080/index.html you'll see the Swagger page with available APIs:

swagger

You can call the first API, and you'll get an empty collection:

all

Since you don't have any tables, they will be created and saved locally for your project. If you need to clear data, you can delete the DB file.

project

Let's post data:

post

success

If you get all the data, you'll see this:

all result

If you go to the http://localhost:8001 link, you'll see the admin panel with tables:

tables

If you click on the table, you'll see the data:

data

You can see the metadata or delete the table:

meta

If you click by the item, you'll see this page:

item

You can add new items based on the previous:

new item

Also, you can delete items:

items

You can filter and manage records without additional endpoints.

Conclutions

It's an excellent approach to development and testing. On DynamoDB locally, you can quickly delete and create items without an active subscription. You only need to update the app settings file.

I hope my article is helpful for you and see you next week. Happy coding!

As usual, you can find the source code by the link.

Buy Me A Beer

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .