Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
26 changes: 18 additions & 8 deletions Aspire.sln
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.MySqlConnector.Tests
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestProject.IntegrationServiceA", "tests\testproject\TestProject.IntegrationServiceA\TestProject.IntegrationServiceA.csproj", "{DCF2D47A-921A-4900-B5B2-CF97B3531CE8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.MongoDB.Driver", "src\Components\Aspire.MongoDB.Driver\Aspire.MongoDB.Driver.csproj", "{20A5A907-A135-4735-B4BF-E13514F360E3}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.MongoDB.Driver", "src\Components\Aspire.MongoDB.Driver\Aspire.MongoDB.Driver.csproj", "{20A5A907-A135-4735-B4BF-E13514F360E3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.MongoDB.Driver.Tests", "tests\Aspire.MongoDB.Driver.Tests\Aspire.MongoDB.Driver.Tests.csproj", "{E592E447-BA3C-44FA-86C1-EBEDC864A644}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.MongoDB.Driver.Tests", "tests\Aspire.MongoDB.Driver.Tests\Aspire.MongoDB.Driver.Tests.csproj", "{E592E447-BA3C-44FA-86C1-EBEDC864A644}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject.LaunchSettings", "tests\testproject\TestProject.LaunchSettings\TestProject.LaunchSettings.csproj", "{A734177E-213B-4D68-98A4-6F5C00234053}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestProject.LaunchSettings", "tests\testproject\TestProject.LaunchSettings\TestProject.LaunchSettings.csproj", "{A734177E-213B-4D68-98A4-6F5C00234053}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Oracle.EntityFrameworkCore.Database", "src\Components\Aspire.Oracle.EntityFrameworkCore.Database\Aspire.Oracle.EntityFrameworkCore.Database.csproj", "{A778F29A-6C40-4C53-A793-F23F20679ADE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Oracle.EntityFrameworkCore.Database.Tests", "tests\Aspire.Oracle.EntityFrameworkCore.Database.Tests\Aspire.Oracle.EntityFrameworkCore.Database.Tests.csproj", "{A331C123-35A5-4E81-9999-354159821374}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -445,14 +449,18 @@ Global
{E592E447-BA3C-44FA-86C1-EBEDC864A644}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E592E447-BA3C-44FA-86C1-EBEDC864A644}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E592E447-BA3C-44FA-86C1-EBEDC864A644}.Release|Any CPU.Build.0 = Release|Any CPU
{6472D59F-7C04-43DE-AD33-9F20BE3804BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6472D59F-7C04-43DE-AD33-9F20BE3804BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6472D59F-7C04-43DE-AD33-9F20BE3804BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6472D59F-7C04-43DE-AD33-9F20BE3804BF}.Release|Any CPU.Build.0 = Release|Any CPU
{A734177E-213B-4D68-98A4-6F5C00234053}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A734177E-213B-4D68-98A4-6F5C00234053}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A734177E-213B-4D68-98A4-6F5C00234053}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A734177E-213B-4D68-98A4-6F5C00234053}.Release|Any CPU.Build.0 = Release|Any CPU
{A778F29A-6C40-4C53-A793-F23F20679ADE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A778F29A-6C40-4C53-A793-F23F20679ADE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A778F29A-6C40-4C53-A793-F23F20679ADE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A778F29A-6C40-4C53-A793-F23F20679ADE}.Release|Any CPU.Build.0 = Release|Any CPU
{A331C123-35A5-4E81-9999-354159821374}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A331C123-35A5-4E81-9999-354159821374}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A331C123-35A5-4E81-9999-354159821374}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A331C123-35A5-4E81-9999-354159821374}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -527,10 +535,12 @@ Global
{6472D59F-7C04-43DE-AD33-9F20BE3804BF} = {975F6F41-B455-451D-A312-098DE4A167B6}
{CA283D7F-EB95-4353-B196-C409965D2B42} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2}
{C8079F06-304F-49B1-A0C1-45AA3782A923} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
{DCF2D47A-921A-4900-B5B2-CF97B3531CE8} = {975F6F41-B455-451D-A312-098DE4A167B6}
{20A5A907-A135-4735-B4BF-E13514F360E3} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2}
{E592E447-BA3C-44FA-86C1-EBEDC864A644} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
{DCF2D47A-921A-4900-B5B2-CF97B3531CE8} = {975F6F41-B455-451D-A312-098DE4A167B6}
{A734177E-213B-4D68-98A4-6F5C00234053} = {975F6F41-B455-451D-A312-098DE4A167B6}
{A778F29A-6C40-4C53-A793-F23F20679ADE} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2}
{A331C123-35A5-4E81-9999-354159821374} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6DCEDFEC-988E-4CB3-B45B-191EB5086E0C}
Expand Down
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
<PackageVersion Include="MySqlConnector.DependencyInjection" Version="2.3.1" />
<PackageVersion Include="Npgsql.DependencyInjection" Version="8.0.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageVersion Include="Oracle.EntityFrameworkCore" Version="8.21.121" />
<PackageVersion Include="Polly" Version="8.2.0" />
<PackageVersion Include="RabbitMQ.Client" Version="6.7.0" />
<PackageVersion Include="StackExchange.Redis" Version="2.7.4" />
Expand All @@ -102,4 +103,4 @@
<PackageVersion Include="Microsoft.Signed.Wix" Version="1.0.0-v3.14.0.5722" />
<PackageVersion Include="Microsoft.DotNet.Build.Tasks.Installers" Version="8.0.0-beta.23564.4" />
</ItemGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
Copy link
Member

Choose a reason for hiding this comment

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

Why the .Database suffix? The NuGet package from Oracle does not have this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried to follow the existing pattern of other EF components where we have Aspire.X.EntityFrameworkCore.Y where:

X is the company/project
Y is the product

Oracle calls its product Oracle Database, so I followed this pattern to leave it open for other Oracle products that may support EntityFramework.

Aspire.Oracle.EntityFrameworkCore.Database
Aspire.Microsoft.EntityFrameworkCore.Cosmos
Aspire.Microsoft.EntityFrameworkCore.SqlServer
Aspire.Npgsql.EntityFrameworkCore.PostgreSQL

Copy link
Member

Choose a reason for hiding this comment

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

Fair enough. I'm happy with that as a justification as long as @eerhardt is happy as the czar of Aspire components ;)

Copy link
Member

Choose a reason for hiding this comment

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

The naming pattern we use is in

## Naming
- Each component's name must have the prefix `Aspire.`.
- When component is built around `ABC` client library, it should contain the client library name in its name. Example: `Aspire.ABC`. Where the technology has a particular casing we have preferred that: for example `Aspire.RabbitMQ` rather than `Aspire.RabbitMq`.

This should be called Aspire.Oracle.EntityFrameworkCore.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done


<PropertyGroup>
<TargetFramework>$(NetCurrent)</TargetFramework>
<IsPackable>true</IsPackable>
<PackageTags>$(ComponentEfCorePackageTags) sqlserver sql</PackageTags>
<Description>A Oracle Database provider for Entity Framework Core that integrates with Aspire, including connection pooling, health check, logging, and telemetry.</Description>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\Common\HealthChecksExtensions.cs" Link="HealthChecksExtensions.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
<PackageReference Include="OpenTelemetry.Instrumentation.EntityFrameworkCore" />
<PackageReference Include="OpenTelemetry.Instrumentation.EventCounters" />
<PackageReference Include="Oracle.EntityFrameworkCore" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Aspire;
using Aspire.Oracle.EntityFrameworkCore.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
using Oracle.EntityFrameworkCore;

namespace Microsoft.Extensions.Hosting;

/// <summary>
/// Extension methods for configuring EntityFrameworkCore DbContext to Oracle database
/// </summary>
public static class AspireOracleEFCoreDatabaseExtensions
{
private const string DefaultConfigSectionName = "Aspire:Oracle:EntityFrameworkCore:Database";
private const DynamicallyAccessedMemberTypes RequiredByEF = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties;

/// <summary>
/// Registers the given <see cref="DbContext" /> as a service in the services provided by the <paramref name="builder"/>.
/// Configures the connection pooling, health check, logging and telemetry for the <see cref="DbContext" />.
/// </summary>
/// <typeparam name="TContext">The <see cref="DbContext" /> that needs to be registered.</typeparam>
/// <param name="builder">The <see cref="IHostApplicationBuilder" /> to read config from and add services to.</param>
/// <param name="connectionName">A name used to retrieve the connection string from the ConnectionStrings configuration section.</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>
/// <param name="configureDbContextOptions">An optional delegate to configure the <see cref="DbContextOptions"/> for the context.</param>
/// <remarks>Reads the configuration from "Aspire:Oracle:EntityFrameworkCore:Database:{typeof(TContext).Name}" config section, or "Aspire:Oracle:EntityFrameworkCore:Database" if former does not exist.</remarks>
/// <exception cref="ArgumentNullException">Thrown if mandatory <paramref name="builder"/> is null.</exception>
/// <exception cref="InvalidOperationException">Thrown when mandatory <see cref="OracleEntityFrameworkCoreDatabaseSettings.ConnectionString"/> is not provided.</exception>
public static void AddOracleDatabaseDbContext<[DynamicallyAccessedMembers(RequiredByEF)] TContext>(
this IHostApplicationBuilder builder,
string connectionName,
Action<OracleEntityFrameworkCoreDatabaseSettings>? configureSettings = null,
Action<DbContextOptionsBuilder>? configureDbContextOptions = null) where TContext : DbContext
{
ArgumentNullException.ThrowIfNull(builder);

OracleEntityFrameworkCoreDatabaseSettings settings = new();
var typeSpecificSectionName = $"{DefaultConfigSectionName}:{typeof(TContext).Name}";
var typeSpecificConfigurationSection = builder.Configuration.GetSection(typeSpecificSectionName);
if (typeSpecificConfigurationSection.Exists()) // https://github.com/dotnet/runtime/issues/91380
{
typeSpecificConfigurationSection.Bind(settings);
}
else
{
builder.Configuration.GetSection(DefaultConfigSectionName).Bind(settings);
}

if (builder.Configuration.GetConnectionString(connectionName) is string connectionString)
{
settings.ConnectionString = connectionString;
}

configureSettings?.Invoke(settings);

if (settings.DbContextPooling)
{
builder.Services.AddDbContextPool<TContext>(ConfigureDbContext);
}
else
{
builder.Services.AddDbContext<TContext>(ConfigureDbContext);
}

if (settings.Tracing)
{
builder.Services.AddOpenTelemetry().WithTracing(tracerProviderBuilder =>
{
tracerProviderBuilder.AddEntityFrameworkCoreInstrumentation();
});
}

if (settings.Metrics)
{
builder.Services.AddOpenTelemetry().WithMetrics(meterProviderBuilder =>
{
meterProviderBuilder.AddEventCountersInstrumentation(eventCountersInstrumentationOptions =>
{
eventCountersInstrumentationOptions.AddEventSources("Oracle.EntityFrameworkCore.Database");
});
});
}

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

void ConfigureDbContext(DbContextOptionsBuilder dbContextOptionsBuilder)
{
if (string.IsNullOrEmpty(settings.ConnectionString))
{
throw new InvalidOperationException($"ConnectionString is missing. It should be provided in 'ConnectionStrings:{connectionName}' or under the 'ConnectionString' key in '{DefaultConfigSectionName}' or '{typeSpecificSectionName}' configuration section.");
}

dbContextOptionsBuilder.UseOracle(settings.ConnectionString, builder =>
{
// Resiliency:
// Connection resiliency automatically retries failed database commands
if (settings.MaxRetryCount > 0)
{
builder.ExecutionStrategy(context => new OracleRetryingExecutionStrategy(context, settings.MaxRetryCount));
}

// The time in seconds to wait for the command to execute.
if (settings.Timeout.HasValue)
{
builder.CommandTimeout(settings.Timeout);
}
});

configureDbContextOptions?.Invoke(dbContextOptionsBuilder);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
{
"definitions": {
"logLevel": {
"properties": {
"Microsoft.EntityFrameworkCore": {
"$ref": "#/definitions/logLevelThreshold"
},
"Microsoft.EntityFrameworkCore.ChangeTracking": {
"$ref": "#/definitions/logLevelThreshold"
},
"Microsoft.EntityFrameworkCore.Database": {
"$ref": "#/definitions/logLevelThreshold"
},
"Microsoft.EntityFrameworkCore.Database.Command": {
"$ref": "#/definitions/logLevelThreshold"
},
"Microsoft.EntityFrameworkCore.Database.Transaction": {
"$ref": "#/definitions/logLevelThreshold"
},
"Microsoft.EntityFrameworkCore.Database.Connection": {
"$ref": "#/definitions/logLevelThreshold"
},
"Microsoft.EntityFrameworkCore.Infrastructure": {
"$ref": "#/definitions/logLevelThreshold"
},
"Microsoft.EntityFrameworkCore.Migrations": {
"$ref": "#/definitions/logLevelThreshold"
},
"Microsoft.EntityFrameworkCore.Model": {
"$ref": "#/definitions/logLevelThreshold"
},
"Microsoft.EntityFrameworkCore.Model.Validation": {
"$ref": "#/definitions/logLevelThreshold"
},
"Microsoft.EntityFrameworkCore.Query": {
"$ref": "#/definitions/logLevelThreshold"
},
"Microsoft.EntityFrameworkCore.Update": {
"$ref": "#/definitions/logLevelThreshold"
}
}
}
},
"properties": {
"Aspire": {
"type": "object",
"properties": {
"Oracle": {
"type": "object",
"properties": {
"EntityFrameworkCore": {
"type": "object",
"properties": {
"Database": {
"type": "object",
"properties": {
"ConnectionString": {
"type": "string",
"description": "Gets or sets the connection string of the SQL Server database to connect to."
},
"DbContextPooling": {
"type": "boolean",
"description": "Gets or sets a boolean value that indicates whether the DbContext will be pooled or explicitly created every time it's requested."
},
"MaxRetryCount": {
"type": "integer",
"description": "Gets or sets the maximum number of retry attempts. Set it to 0 to disable the retry mechanism.",
"default": 6
},
"HealthChecks": {
"type": "boolean",
"description": "Gets or sets a boolean value that indicates whether the DbContext health check is enabled or not.",
"default": true
},
"Tracing": {
"type": "boolean",
"description": "Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is enabled or not.",
"default": true
},
"Metrics": {
"type": "boolean",
"description": "Gets or sets a boolean value that indicates whether the OpenTelemetry metrics are enabled or not.",
"default": true
},
"Timeout": {
"type": "integer",
"description": "Gets or sets the time in seconds to wait for the command to execute.",
"default": null
}
}
}
}
}
}
}
}
}
},
"type": "object"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Oracle.EntityFrameworkCore.Database;

/// <summary>
/// Provides the client configuration settings for connecting to a Oracle database using EntityFrameworkCore.
/// </summary>
public sealed class OracleEntityFrameworkCoreDatabaseSettings
{
/// <summary>
/// The connection string of the Oracle database to connect to.
/// </summary>
public string? ConnectionString { get; set; }

/// <summary>
/// Gets or sets a boolean value that indicates whether the db context will be pooled or explicitly created every time it's requested.
/// </summary>
public bool DbContextPooling { get; set; } = true;

/// <summary>
/// <para>Gets or sets the maximum number of retry attempts.</para>
/// <value>
/// The default is 6.
/// Set it to 0 to disable the retry mechanism.
/// </value>
/// </summary>
public int MaxRetryCount { get; set; } = 6;

/// <summary>
/// <para>Gets or sets a boolean value that indicates whether the database health check is enabled or not.</para>
/// <value>
/// The default value is <see langword="true"/>.
/// </value>
/// </summary>
public bool HealthChecks { get; set; } = true;

/// <summary>
/// <para>Gets or sets a boolean value that indicates whether the Open Telemetry tracing is enabled or not.</para>
/// <value>
/// The default value is <see langword="true"/>.
/// </value>
/// </summary>
public bool Tracing { get; set; } = true;

/// <summary>
/// <para>Gets or sets a boolean value that indicates whether the Open Telemetry metrics are enabled or not.</para>
/// <value>
/// The default value is <see langword="true"/>.
/// </value>
/// </summary>
public bool Metrics { get; set; } = true;

/// <summary>
/// The time in seconds to wait for the command to execute.
/// </summary>
public int? Timeout { get; set; }
}
Loading