-
Notifications
You must be signed in to change notification settings - Fork 253
/
Copy pathProgram.cs
140 lines (120 loc) · 5 KB
/
Program.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
using Microsoft.Azure.Cosmos;
using Microsoft.Azure.Cosmos.Linq;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Polly;
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddProblemDetails();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument();
builder.AddAzureCosmosClient("cosmos");
builder.Services.AddSingleton<DatabaseBootstrapper>();
builder.Services.AddHealthChecks()
.Add(new("cosmos", sp => sp.GetRequiredService<DatabaseBootstrapper>(), null, null));
builder.Services.AddHostedService(sp => sp.GetRequiredService<DatabaseBootstrapper>());
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseOpenApi();
app.UseSwaggerUi();
}
app.UseExceptionHandler();
// Create new todos
app.MapPost("/todos", async (Todo todo, CosmosClient cosmosClient) =>
(await cosmosClient.GetAppDataContainer().CreateItemAsync(todo)).Resource
);
// Get all the todos
app.MapGet("/todos", (CosmosClient cosmosClient) =>
cosmosClient.GetAppDataContainer()
.GetItemLinqQueryable<Todo>()
.ToFeedIterator()
.ToAsyncEnumerable()
);
app.MapPut("/todos/{id}", async (string id, Todo todo, CosmosClient cosmosClient) =>
(await cosmosClient.GetAppDataContainer().ReplaceItemAsync(todo, id)).Resource
);
app.MapDelete("/todos/{userId}/{id}", async (string userId, string id, CosmosClient cosmosClient) =>
{
await cosmosClient.GetAppDataContainer().DeleteItemAsync<Todo>(id, new PartitionKey(userId));
return Results.Ok();
});
app.MapDefaultEndpoints();
app.Run();
// The Todo service model used for transmitting data
public record Todo(string Description, string id, string UserId, bool IsComplete = false)
{
// partiion the todos by user id
internal static string UserIdPartitionKey = "/UserId";
}
// Background service used to scaffold the Cosmos DB/Container
public class DatabaseBootstrapper(CosmosClient cosmosClient, ILogger<DatabaseBootstrapper> logger) : BackgroundService, IHealthCheck
{
private bool _dbCreated;
private bool _dbCreationFailed;
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
var status = _dbCreated
? HealthCheckResult.Healthy()
: _dbCreationFailed
? HealthCheckResult.Unhealthy("Database creation failed.")
: HealthCheckResult.Degraded("Database creation is still in progress.");
return Task.FromResult(status);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// The Cosmos DB emulator can take a very long time to start (multiple minutes) so use a custom resilience strategy
// to ensure it retries long enough.
var retry = new ResiliencePipelineBuilder()
.AddRetry(new()
{
Delay = TimeSpan.FromSeconds(5),
MaxRetryAttempts = 60,
BackoffType = DelayBackoffType.Constant,
OnRetry = args =>
{
logger.LogWarning("""
Issue during database creation after {AttemptDuration} on attempt {AttemptNumber}. Will retry in {RetryDelay}.
Exception:
{ExceptionMessage}
{InnerExceptionMessage}
""",
args.Duration,
args.AttemptNumber,
args.RetryDelay,
args.Outcome.Exception?.Message ?? "[none]",
args.Outcome.Exception?.InnerException?.Message ?? "");
return ValueTask.CompletedTask;
}
})
.Build();
await retry.ExecuteAsync(async ct =>
{
await cosmosClient.CreateDatabaseIfNotExistsAsync("tododb", cancellationToken: ct);
var database = cosmosClient.GetDatabase("tododb");
await database.CreateContainerIfNotExistsAsync(new ContainerProperties("todos", Todo.UserIdPartitionKey), cancellationToken: ct);
logger.LogInformation("Database successfully created!");
_dbCreated = true;
}, stoppingToken);
_dbCreationFailed = !_dbCreated;
}
}
// Convenience class for reusing boilerplate code
public static class CosmosClientTodoAppExtensions
{
public static Container GetAppDataContainer(this CosmosClient cosmosClient)
{
var database = cosmosClient.GetDatabase("tododb");
var todos = database.GetContainer("todos") ?? throw new ApplicationException("Cosmos DB collection missing.");
return todos;
}
public static async IAsyncEnumerable<TModel> ToAsyncEnumerable<TModel>(this FeedIterator<TModel> setIterator)
{
while (setIterator.HasMoreResults)
{
foreach (var item in await setIterator.ReadNextAsync())
{
yield return item;
}
}
}
}