Skip to content
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

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
eaf669f
added cosmosdb sample
bradygaster Jun 13, 2024
9fcea51
Update samples/AspireWithCosmosDb/README.md
bradygaster Jun 14, 2024
9862c1f
updated image per feedback
bradygaster Jun 14, 2024
40eec45
Merge branch 'bradyg/cosmosdb' of https://github.com/dotnet/aspire-sa…
bradygaster Jun 14, 2024
72fa8f9
Update samples/AspireWithCosmosDb/AspireWithCosmos.ApiService/Program.cs
bradygaster Jun 14, 2024
bc8d579
Update samples/AspireWithCosmosDb/AspireWithCosmos.AppHost/AspireWith…
bradygaster Jun 14, 2024
c04b9e3
formatting
bradygaster Jun 14, 2024
d3d82be
Merge branch 'bradyg/cosmosdb' of https://github.com/dotnet/aspire-sa…
bradygaster Jun 14, 2024
1883ca0
more formatting per the ide's suggestions
bradygaster Jun 14, 2024
4637d7e
Update samples/AspireWithCosmosDb/AspireWithCosmos.Web/TodoApiClient.cs
bradygaster Jun 14, 2024
0a0d082
Update samples/AspireWithCosmosDb/AspireWithCosmos.Tests/AspireWithCo…
bradygaster Jun 14, 2024
c909987
Update samples/AspireWithCosmosDb/AspireWithCosmos.ServiceDefaults/As…
bradygaster Jun 14, 2024
4d22886
Update samples/AspireWithCosmosDb/AspireWithCosmos.ServiceDefaults/As…
bradygaster Jun 14, 2024
48a4de5
removed test project per feedback.
bradygaster Jun 14, 2024
f91372c
Merge branch 'bradyg/cosmosdb' of https://github.com/dotnet/aspire-sa…
bradygaster Jun 14, 2024
ec5f76e
simplified terminal image
bradygaster Jun 14, 2024
789446c
added emulator call and comment.
bradygaster Jun 14, 2024
ba4f8bb
added cosmos db to tests
bradygaster Jun 19, 2024
339c7d4
updated an otel lib
bradygaster Jun 21, 2024
c349ac1
one more tweak
bradygaster Jun 21, 2024
1066a07
Bump packages & other tweaks
DamianEdwards Jun 21, 2024
c035293
Add retry policy & health check for Cosmos DB db creation
DamianEdwards Jun 21, 2024
d27bf16
Update samples/AspireWithCosmosDb/AspireWithCosmos.ApiService/AspireW…
bradygaster Jun 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 74 additions & 10 deletions samples/AspireWithCosmosDb/AspireWithCosmos.ApiService/Program.cs
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));
Comment on lines +15 to +16
Copy link
Member

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)

Copy link
Member

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.

builder.Services.AddHostedService(sp => sp.GetRequiredService<DatabaseBootstrapper>());

var app = builder.Build();

Expand All @@ -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) =>
Expand All @@ -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
Copy link
Member Author

Choose a reason for hiding this comment

The 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!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should put this code in the component.

cc @eerhardt

Copy link
Member

@eerhardt eerhardt Jun 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. @Pilchie mentioned offline that they were going to change the emulator to not respond until it was ready. That would solve the issues with getting SSL errors during startup.
  2. We could add this to the CosmosClientOptions.HttpFactory by default. It would be better if the library had resilience built into it though.

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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. :)

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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
Expand All @@ -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;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
var builder = DistributedApplication.CreateBuilder(args);
using Aspire.Hosting;

var builder = DistributedApplication.CreateBuilder(args);

var tododb = builder.AddAzureCosmosDB("cosmos").AddDatabase("tododb")
.RunAsEmulator(); // Remove this line should you want to use a live instance during development
// or should you not have the Azure Cosmos DB emulator installed.
// Remove the RunAsEmulator() line should you want to use a live instance during development
.RunAsEmulator();

var apiService = builder.AddProject<Projects.AspireWithCosmos_ApiService>("apiservice")
.WithReference(tododb);
Expand Down
Loading