AspNetCore.Identity.AmazonDynamoDB
3.0.0
dotnet add package AspNetCore.Identity.AmazonDynamoDB --version 3.0.0
NuGet\Install-Package AspNetCore.Identity.AmazonDynamoDB -Version 3.0.0
<PackageReference Include="AspNetCore.Identity.AmazonDynamoDB" Version="3.0.0" />
<PackageVersion Include="AspNetCore.Identity.AmazonDynamoDB" Version="3.0.0" />
<PackageReference Include="AspNetCore.Identity.AmazonDynamoDB" />
paket add AspNetCore.Identity.AmazonDynamoDB --version 3.0.0
#r "nuget: AspNetCore.Identity.AmazonDynamoDB, 3.0.0"
#:package AspNetCore.Identity.AmazonDynamoDB@3.0.0
#addin nuget:?package=AspNetCore.Identity.AmazonDynamoDB&version=3.0.0
#tool nuget:?package=AspNetCore.Identity.AmazonDynamoDB&version=3.0.0
AspNetCore.Identity.AmazonDynamoDB
An ASP.NET Core Identity provider for DynamoDB, targeting .NET 8, 9 and 10. On .NET 10 it also stores passkey (WebAuthn) credentials through IUserPasskeyStore<TUser>.
Getting Started
You can install the latest version via Nuget:
> dotnet add package AspNetCore.Identity.AmazonDynamoDB
Then you use the stores by calling AddDynamoDbStores on IdentityBuilder, with the user type and, when roles are used, the role type:
services
.AddIdentityCore<DynamoDbUser>()
.AddRoles<DynamoDbRole>()
.AddDynamoDbStores<DynamoDbUser, DynamoDbRole>()
.Configure(options =>
{
options.BillingMode = BillingMode.PROVISIONED; // Default is BillingMode.PAY_PER_REQUEST
options.ProvisionedThroughput = new ProvisionedThroughput
{
ReadCapacityUnits = 5, // Default is 1
WriteCapacityUnits = 5, // Default is 1
};
options.DefaultTableName = "my-custom-identity-table-name"; // Default is identity
options.ResolveRoleNames = true; // Default is false, see Role Names below
});
Finally, you need to ensure that tables and indexes have been added:
AspNetCoreIdentityDynamoDbSetup.EnsureInitialized(serviceProvider);
Or asynchronously:
await AspNetCoreIdentityDynamoDbSetup.EnsureInitializedAsync(serviceProvider);
The table is created if it doesn't exist. If it already exists, any global secondary index that is missing from it (such as CredentialId-index, used to look up users by passkey) is added and the call waits until the index is active. New indexes use provisioned throughput from the options when the existing table is provisioned, and on-demand capacity otherwise. It's safe to call from several instances at the same time.
The call needs the dynamodb:DescribeTable permission on the table, plus dynamodb:CreateTable to create it and dynamodb:UpdateTable to add a missing index. Without dynamodb:DescribeTable it falls back to dynamodb:ListTables and only checks that the table exists. If an index can't be added because dynamodb:UpdateTable is missing, a warning is logged instead of failing.
The IServiceProvider overloads log progress and warnings through the registered ILoggerFactory.
Role Names
Users store the normalized names of their roles, so by default UserManager.GetRolesAsync returns ADMIN for a role named Admin, and that is also the value of the role claims in the user's cookie. ClaimsPrincipal.IsInRole and [Authorize(Roles = "...")] compare role names case sensitively, so they need to use the normalized name.
Set ResolveRoleNames (or call ResolveRoleNames() on the builder) to return the names of the roles instead, as the Entity Framework Core stores do. Each role is then looked up by its normalized name when the user's roles are read, and roles that can't be found are returned with their normalized name.
Tests
To run the tests, you need to have DynamoDB running locally on localhost:8000. This can easily be done using Docker and the following command:
docker run -p 8000:8000 amazon/dynamodb-local
Table Layout
Everything lives in one table with a PartitionKey and a SortKey and no secondary indexes. A user's items share its partition (USER#{id}): the user itself, its claims, logins, roles, tokens and passkeys. Values that are looked up directly have a lookup item of their own, which is one strongly consistent read: USERNAME#{normalized}, LOGIN#{provider}#{key}, PASSKEY#{credentialId} and ROLENAME#{normalized} point at the id they belong to, and the partitions EMAIL#{normalized}, CLAIM#{type}#{value} and USERROLE#{name} list the users that hold them. A user and its username lookup are written in one transaction, as are a role and its name lookup and each login or passkey with its lookup, so a username, role name, login or passkey that another user holds fails the write with Identity's DuplicateUserName, DuplicateRoleName, LoginAlreadyAssociated or a DuplicatePasskey error. Emails are not unique, as Identity leaves that to RequireUniqueEmail; FindByEmailAsync returns one of the users with the email.
Upgrading from 2.x
Version 2 found users and roles through seven global secondary indexes. Version 3 reads and writes the same items in the same format, so no data changes, but it needs the lookup items described above, which an existing table does not have. Upgrade like this:
- With version 3 installed, run
AspNetCoreIdentityDynamoDbSetup.MigrateAsync(serviceProvider)(orDynamoDbTableSetup.MigrateAsync(options, client)) against the table. It scans the table once and writes the lookup items for every user, role, claim, login, role membership and passkey. It is safe to run while version 2 instances are serving traffic, and safe to run again. The result says how many items it wrote lookups for. - Deploy version 3.
EnsureInitializedlogs a warning as long as the legacy indexes exist, and nothing else; it never scans or changes an existing table. - Run
MigrateAsynconce more, to cover anything a version 2 instance wrote during the rollout. - Run
AspNetCoreIdentityDynamoDbSetup.RemoveLegacyIndexesAsync(serviceProvider)to drop the seven indexes, one at a time as DynamoDB requires. Until then the indexes are kept up to date by DynamoDB and cost storage and write capacity, but nothing reads them. This needsdynamodb:UpdateTable; without it a warning is logged and the indexes stay.
What changes for an application:
- Lookups by name, email, login or passkey are two strongly consistent reads instead of one eventually consistent index query, so a user is found right after it was created.
- Creating or updating a user is a transaction of the user item and its lookups, which DynamoDB bills at twice the write units of a plain write. Duplicate usernames, role names, logins and passkeys fail with the Identity errors above instead of relying on the managers' checks.
- Register the stores with
AddDynamoDbStores<TUser>()orAddDynamoDbStores<TUser, TRole>(); the non-generic overload still works but needs dynamic code, so it is not for trimmed or Native AOT applications. - Custom properties on a
DynamoDbUserorDynamoDbRolesubclass are stored only when the subclass overridesWriteAttributesandReadAttributes, see Adding Attributes. DynamoDbTableSetup.EnsureAliasCreatedandDynamoDbOptionsMonitorare gone: the stores read the table name from the options on every call and no longer use the SDK's object persistence model.
Native AOT
The package is trim and Native AOT compatible: the stores read and write items through the low-level DynamoDB client, with no reflection. Register the stores with the generic AddDynamoDbStores<TUser>() or AddDynamoDbStores<TUser, TRole>(); the non-generic AddDynamoDbStores() builds the store types from the builder's user and role types at runtime and is marked as requiring dynamic code.
Adding Attributes
To add custom attributes to the user or role model, create a class that extends DynamoDbUser or DynamoDbRole, add the properties, and override WriteAttributes and ReadAttributes to store them. The AttributeValues extension methods convert strings, booleans, numbers, dates, lists and maps the way the store does.
public class CustomUser : DynamoDbUser
{
public string? ProfilePictureUrl { get; set; }
protected override void WriteAttributes(Dictionary<string, AttributeValue> item)
{
base.WriteAttributes(item);
item.Set("ProfilePictureUrl", ProfilePictureUrl);
}
protected override void ReadAttributes(Dictionary<string, AttributeValue> item)
{
base.ReadAttributes(item);
ProfilePictureUrl = item.GetString("ProfilePictureUrl");
}
}
Then use your classes when adding the DynamoDB stores:
services
.AddIdentityCore<CustomUser>()
.AddRoles<CustomRole>()
.AddDynamoDbStores<CustomUser, CustomRole>();
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- AWSSDK.DynamoDBv2 (>= 4.0.109.3)
- Microsoft.Extensions.Identity.Stores (>= 10.0.14)
-
net8.0
- AWSSDK.DynamoDBv2 (>= 4.0.109.3)
- Microsoft.Extensions.Identity.Stores (>= 8.0.33)
-
net9.0
- AWSSDK.DynamoDBv2 (>= 4.0.109.3)
- Microsoft.Extensions.Identity.Stores (>= 9.0.22)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.0.0 | 127 | 10/28/2026 |
| 2.2.0 | 297 | 9/30/2026 |
| 2.1.0 | 107 | 9/30/2026 |
| 2.0.1 | 733 | 10/28/2025 |
| 2.0.0 | 697 | 2/2/2025 |
| 1.2.4 | 1,110 | 10/4/2024 |
| 1.2.3 | 256 | 10/4/2024 |
| 1.2.1 | 330 | 9/26/2024 |
| 1.2.0 | 969 | 7/7/2024 |
| 1.1.3 | 1,096 | 3/23/2024 |
| 1.1.2 | 356 | 1/22/2024 |
| 1.1.1 | 280 | 1/21/2024 |
| 1.1.0 | 361 | 11/27/2023 |
| 1.0.2 | 604 | 10/29/2023 |
| 1.0.1 | 1,292 | 1/28/2023 |
| 1.0.0 | 556 | 1/6/2023 |
| 0.2.0 | 633 | 10/15/2022 |
| 0.1.3 | 597 | 10/5/2022 |
| 0.1.2 | 537 | 10/5/2022 |