Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<AspireAppHostSdkVersion>$(AspireVersion)</AspireAppHostSdkVersion>
<AspNetCoreVersion>9.0.0</AspNetCoreVersion>
<DotNetExtensionsVersion>9.0.4</DotNetExtensionsVersion>
<OpenTelemetryVersion>1.11.1</OpenTelemetryVersion>
<OpenTelemetryVersion>1.12.0</OpenTelemetryVersion>
<TestContainersVersion>4.4.0</TestContainersVersion>
<MEAIVersion>9.5.0</MEAIVersion>
<IsPackable>false</IsPackable>
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="$(OpenTelemetryVersion)" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="$(OpenTelemetryVersion)" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="$(OpenTelemetryVersion)" />
<PackageVersion Include="OpenTelemetry.Instrumentation.EntityFrameworkCore" Version="1.12.0-beta.2" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="$(OpenTelemetryVersion)" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="$(OpenTelemetryVersion)" />
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="$(OpenTelemetryVersion)" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using Aspire;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics.Internal;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry.Trace;
using System.Diagnostics.CodeAnalysis;

namespace Microsoft.Extensions.Hosting;
Expand Down Expand Up @@ -58,10 +58,7 @@ public static class AspireEFSqliteExtensions

builder.Services.AddDbContextPool<TContext>(ConfigureDbContext);

if (!settings.DisableHealthChecks)
{
builder.TryAddHealthCheck(name: typeof(TContext).Name, static hcBuilder => hcBuilder.AddDbContextCheck<TContext>());
}
ConfigureInstrumentation<TContext>(builder, settings);

void ConfigureDbContext(DbContextOptionsBuilder dbContextOptionsBuilder)
{
Expand All @@ -72,4 +69,72 @@ void ConfigureDbContext(DbContextOptionsBuilder dbContextOptionsBuilder)
configureDbContextOptions?.Invoke(dbContextOptionsBuilder);
}
}

/// <summary>
/// Enriches a <see cref="IHostApplicationBuilder"/> to register the <typeparamref name="TDbContext"/> as a scoped service
/// with simplified configuration and optional OpenTelemetry instrumentation.
/// </summary>
/// <typeparam name="TDbContext">The type of the <see cref="DbContext"/>.</typeparam>
/// <param name="builder">The <see cref="IHostApplicationBuilder"/> to read config from and add services to.</param>
/// <param name="configureSettings">An optional delegate that can be used for customizing options. It's invoked after the settings are read from the configuration.</param>
/// <exception cref="ArgumentNullException">Thrown if mandatory <paramref name="builder"/> is null.</exception>
public static void EnrichSqliteDatabaseDbContext<[DynamicallyAccessedMembers(RequiredByEF)] TDbContext>(
this IHostApplicationBuilder builder,
Action<SqliteEntityFrameworkCoreSettings>? configureSettings = null)
where TDbContext : DbContext
{
ArgumentNullException.ThrowIfNull(builder);

var settings = builder.GetDbContextSettings<TDbContext, SqliteEntityFrameworkCoreSettings>(
DefaultConfigSectionName,
null,
(settings, section) => section.Bind(settings)
);

configureSettings?.Invoke(settings);

Copy link

Copilot AI Sep 11, 2025

Choose a reason for hiding this comment

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

The connection string is not validated before use. Unlike the existing AddSqliteDbContext method which validates the connection string, this new method directly passes settings.ConnectionString to UseSqlite without checking if it's null or empty.

Suggested change
if (string.IsNullOrWhiteSpace(settings.ConnectionString))
{
throw new ArgumentException("The connection string for Sqlite DbContext cannot be null or empty.", nameof(settings.ConnectionString));
}

Copilot uses AI. Check for mistakes.
builder.Services.AddDbContext<TDbContext>(options =>
options.UseSqlite(settings.ConnectionString));
ConfigureInstrumentation<TDbContext>(builder, settings);
}

private static void ConfigureInstrumentation<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] TDbContext>(IHostApplicationBuilder builder, SqliteEntityFrameworkCoreSettings settings) where TDbContext : DbContext
{
if (!settings.DisableTracing)
{
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddEntityFrameworkCoreInstrumentation());
}

if (!settings.DisableHealthChecks)
{
builder.TryAddHealthCheck(
name: typeof(TDbContext).Name,
static hcBuilder => hcBuilder.AddDbContextCheck<TDbContext>());
}
}

internal static TSettings GetDbContextSettings<TContext, TSettings>(this IHostApplicationBuilder builder, string defaultConfigSectionName, string? connectionName, Action<TSettings, IConfiguration> bindSettings)
where TSettings : new()
{
TSettings settings = new();
var configurationSection = builder.Configuration.GetSection(defaultConfigSectionName);
bindSettings(settings, configurationSection);
// If the connectionName is not provided, we've been called in the context
// of an Enrich invocation and don't need to bind the connectionName specific settings.
// Instead, we'll just bind to the TContext-specific settings.
if (connectionName is not null)
{
var connectionSpecificConfigurationSection = configurationSection.GetSection(connectionName);
bindSettings(settings, connectionSpecificConfigurationSection);
}
var typeSpecificConfigurationSection = configurationSection.GetSection(typeof(TContext).Name);
if (typeSpecificConfigurationSection.Exists()) // https://github.com/dotnet/runtime/issues/91380
{
bindSettings(settings, typeSpecificConfigurationSection);
}

return settings;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
<PackageReference Include="OpenTelemetry.Instrumentation.EntityFrameworkCore" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,36 @@ dotnet add package CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite

### Example usage

#### Option 1: Using IHostApplicationBuilder (Traditional Aspire Pattern)

In the _Program.cs_ file of your project, call the `AddSqliteDbContext<TDbContext>` extension method to register the `TDbContext` implementation in the DI container. This method takes the connection name as a parameter:

```csharp
builder.AddSqliteDbContext<BloggingContext>("sqlite");
```

#### Option 2: Using WebApplicationBuilder (New Simplified Pattern)

For ASP.NET Core applications, you can use the simplified `EnrichSqliteDatabaseDbContext<TDbContext>` extension method:

```csharp
// Basic usage with default connection string name "DefaultConnection"
builder.EnrichSqliteDatabaseDbContext<BloggingContext>();

// With custom connection string name
builder.EnrichSqliteDatabaseDbContext<BloggingContext>("MyConnection");

// Disable OpenTelemetry instrumentation
builder.EnrichSqliteDatabaseDbContext<BloggingContext>(enableOpenTelemetry: false);
```

The `EnrichSqliteDatabaseDbContext` method provides:

- **Simplified API**: Works directly with `WebApplicationBuilder`
- **Default connection string**: Uses "DefaultConnection" by default
- **OpenTelemetry integration**: Automatically adds EF Core instrumentation for distributed tracing
- **Parameter validation**: Proper error handling for missing connection strings

Then, in your service, inject `TDbContext` and use it to interact with the database:

```csharp
Expand All @@ -42,3 +66,4 @@ public class MyService(BloggingContext context)
## Feedback & contributing

https://github.com/CommunityToolkit/Aspire

Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,10 @@ public sealed class SqliteEntityFrameworkCoreSettings
/// Gets or sets the default timeout for the database operations.
/// </summary>
public int DefaultTimeout { get; set; }

/// <summary>
/// Gets or sets a boolean value that indicates whether tracing is disabled or not.
/// </summary>
public bool DisableTracing { get; set; }

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace CommunityToolkit.Aspire.Microsoft.EntityFrameworkCore.Sqlite.Tests;

public class EnrichSqliteDatabaseDbContextTests
{
[Fact]
public void EnrichSqliteDatabaseDbContext_RegistersDbContext()
{
// Arrange
var builder = WebApplication.CreateBuilder();
builder.Configuration.AddInMemoryCollection([
new KeyValuePair<string, string?>("ConnectionStrings:DefaultConnection", "Data Source=:memory:")
]);

// Act
builder.EnrichSqliteDatabaseDbContext<TestDbContext>();

// Assert
var app = builder.Build();
var dbContext = app.Services.GetRequiredService<TestDbContext>();
Assert.NotNull(dbContext);
}

[Fact]
public void EnrichSqliteDatabaseDbContext_ThrowsWhenBuilderIsNull()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() =>
AspireEFSqliteExtensions.EnrichSqliteDatabaseDbContext<TestDbContext>(null!));
}

[Fact]
public void EnrichSqliteDatabaseDbContext_DisablesOpenTelemetryWhenFalse()
{
// Arrange
var builder = WebApplication.CreateBuilder();
builder.Configuration.AddInMemoryCollection([
new KeyValuePair<string, string?>("ConnectionStrings:DefaultConnection", "Data Source=:memory:")
]);

// Act
builder.EnrichSqliteDatabaseDbContext<TestDbContext>(settings => settings.DisableTracing = true);
Copy link

Copilot AI Sep 11, 2025

Choose a reason for hiding this comment

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

The test method name suggests it tests the enableOpenTelemetry parameter, but the implementation uses a settings configuration delegate instead. The method signature in the actual implementation doesn't match what's being tested here.

Copilot uses AI. Check for mistakes.

// Assert - The test passes if no exceptions are thrown and DbContext is registered
var app = builder.Build();
var dbContext = app.Services.GetRequiredService<TestDbContext>();
Assert.NotNull(dbContext);
}

[Fact]
public void EnrichSqliteDatabaseDbContext_EnablesOpenTelemetryByDefault()
{
// Arrange
var builder = WebApplication.CreateBuilder();
builder.Configuration.AddInMemoryCollection([
new KeyValuePair<string, string?>("ConnectionStrings:DefaultConnection", "Data Source=:memory:")
]);

// Act
builder.EnrichSqliteDatabaseDbContext<TestDbContext>(settings => settings.DisableTracing = false);
Copy link

Copilot AI Sep 11, 2025

Choose a reason for hiding this comment

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

The test method name suggests it tests the default OpenTelemetry behavior, but it's explicitly setting DisableTracing = false. This test should either call the method without parameters to test the actual default behavior, or rename the test to reflect what it's actually testing.

Suggested change
builder.EnrichSqliteDatabaseDbContext<TestDbContext>(settings => settings.DisableTracing = false);
builder.EnrichSqliteDatabaseDbContext<TestDbContext>();

Copilot uses AI. Check for mistakes.

// Assert - The test passes if no exceptions are thrown and OpenTelemetry services are registered
var app = builder.Build();
var dbContext = app.Services.GetRequiredService<TestDbContext>();
Assert.NotNull(dbContext);

// Verify OpenTelemetry services are registered (basic smoke test)
var services = app.Services.GetServices<IHostedService>().ToList();
Assert.True(services.Count > 0, "Services should be registered");
}
}
Loading