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>
Create entity:
[DynamoDBTable("Users")]
public class User
{
[DynamoDBHashKey]
public required string Id { get; set; }
[DynamoDBProperty]
public required string Name { get; set; }
}
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
}
};
}
}
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);
}
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;
}
}
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();
}
}
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();
});
}
}
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();
Update your appsettings.json
:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"DynamoDb": {
"ServiceUrl": "http://dynamodb-local:8000",
"AccessKey": "YourAccessKey",
"SecretKey": "YourSecretKey"
},
"AllowedHosts": "*"
}
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"]
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
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
Testing
If you open the app by link http://localhost:8080/index.html you'll see the Swagger page with available APIs:
You can call the first API, and you'll get an empty collection:
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.
Let's post data:
If you get all the data, you'll see this:
If you go to the http://localhost:8001 link, you'll see the admin panel with tables:
If you click on the table, you'll see the data:
You can see the metadata or delete the table:
If you click by the item, you'll see this page:
You can add new items based on the previous:
Also, you can delete 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.