-
Notifications
You must be signed in to change notification settings - Fork 253
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
added cosmosdb sample #321
base: main
Are you sure you want to change the base?
Changes from 1 commit
eaf669f
9fcea51
9862c1f
40eec45
72fa8f9
bc8d579
c04b9e3
d3d82be
1883ca0
4637d7e
0a0d082
c909987
4d22886
48a4de5
f91372c
ec5f76e
789446c
ba4f8bb
339c7d4
c349ac1
1066a07
c035293
d27bf16
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,20 @@ | ||
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.AddHostedService<DatabaseBootstrapper>(); | ||
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(); | ||
|
||
|
@@ -19,14 +26,17 @@ | |
|
||
app.UseExceptionHandler(); | ||
|
||
// create new todos | ||
// Create new todos | ||
app.MapPost("/todos", async (Todo todo, CosmosClient cosmosClient) => | ||
(await cosmosClient.GetAppDataContainer().CreateItemAsync(todo)).Resource | ||
); | ||
|
||
// get all the todos | ||
// Get all the todos | ||
app.MapGet("/todos", (CosmosClient cosmosClient) => | ||
cosmosClient.GetAppDataContainer().GetItemLinqQueryable<Todo>(allowSynchronousQueryExecution: true).ToList() | ||
cosmosClient.GetAppDataContainer() | ||
.GetItemLinqQueryable<Todo>() | ||
.ToFeedIterator() | ||
.ToAsyncEnumerable() | ||
); | ||
|
||
app.MapPut("/todos/{id}", async (string id, Todo todo, CosmosClient cosmosClient) => | ||
|
@@ -51,16 +61,59 @@ public record Todo(string Description, string id, string UserId, bool IsComplete | |
} | ||
|
||
// Background service used to scaffold the Cosmos DB/Container | ||
public class DatabaseBootstrapper(CosmosClient cosmosClient) : IHostedService | ||
public class DatabaseBootstrapper(CosmosClient cosmosClient, ILogger<DatabaseBootstrapper> logger) : BackgroundService, IHealthCheck | ||
{ | ||
public async Task StartAsync(CancellationToken cancellationToken) | ||
private bool _dbCreated; | ||
private bool _dbCreationFailed; | ||
|
||
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) | ||
{ | ||
await cosmosClient.CreateDatabaseIfNotExistsAsync("tododb", cancellationToken: cancellationToken); | ||
var database = cosmosClient.GetDatabase("tododb"); | ||
await database.CreateContainerIfNotExistsAsync(new ContainerProperties("todos", Todo.UserIdPartitionKey), cancellationToken: cancellationToken); | ||
var status = _dbCreated | ||
? HealthCheckResult.Healthy() | ||
: _dbCreationFailed | ||
? HealthCheckResult.Unhealthy("Database creation failed.") | ||
: HealthCheckResult.Degraded("Database creation is still in progress."); | ||
return Task.FromResult(status); | ||
} | ||
|
||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cc @Pilchie so he sees how @DamianEdwards was able to work through this - I pinged the team today so mention we'd run into these issues, so wanted to give y'all an update on the completion. Thanks @DamianEdwards! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should put this code in the component. cc @eerhardt There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think if we set the HttpClientFactory option we then own all other aspects setting up the HttpClient instance that the Cosmos client currently handles, including those affected by properties on the connecting string, e.g. disabling the SSL validation check. This is what I ran into when attempting that approach. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to merge this as-is or edit the code in the component in such a manner we can make the sample less-meh? I vote for merging it and then I'll update it once we change the component if @eerhardt informs me once the work is done. :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd agree we should wait for the component changes. FYI - the component is owned by @Pilchie's team. We should work with them to get it updated as necessary. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just wanted to comment that I'm aware of this and following up. I think there is a plan to build some of this into the container itself so that this sort of thing isn't necessary, but I will let you know. cc @kirankumarkolli |
||
// 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 | ||
|
@@ -73,4 +126,15 @@ public static Container GetAppDataContainer(this CosmosClient cosmosClient) | |
|
||
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; | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is odd to have in our sample, when the decision was to not have a health check in the component.
dotnet/aspire#359 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right but this isn't a health check of Cosmos, it's a health check of whether the bootstrapper has completed successfully. Once the bootstrapper has completed this health check is always healthy and doesn't hit Cosmos on its own.