-
Notifications
You must be signed in to change notification settings - Fork 731
Adds Aspire Oracle EntityFrameworkCore Database component. #1295
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
Changes from 2 commits
92a7922
0f2f3f8
12ee396
0f384e8
773b457
06d7a4c
4db7868
824ed28
91e0b7b
74a10a0
ae61db3
9ac8eab
e04cee1
8b84294
5d99c70
7c09cbd
9abcdd8
5f0b689
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 | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,24 @@ | ||||||||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||||||||
|
Member
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. Why the
Contributor
Author
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 tried to follow the existing pattern of other EF components where we have Aspire.X.EntityFrameworkCore.Y where: X is the company/project Oracle calls its product Oracle Database, so I followed this pattern to leave it open for other Oracle products that may support EntityFramework.
Member
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. Fair enough. I'm happy with that as a justification as long as @eerhardt is happy as the czar of Aspire components ;)
Member
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. The naming pattern we use is in aspire/src/Components/README.md Lines 27 to 30 in 23e168d
This should be called
Contributor
Author
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. Done |
||||||||||
|
|
||||||||||
| <PropertyGroup> | ||||||||||
| <TargetFramework>$(NetCurrent)</TargetFramework> | ||||||||||
| <IsPackable>true</IsPackable> | ||||||||||
| <PackageTags>$(ComponentEfCorePackageTags) sqlserver sql</PackageTags> | ||||||||||
eerhardt marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||
| <Description>A Oracle Database provider for Entity Framework Core that integrates with Aspire, including connection pooling, health check, logging, and telemetry.</Description> | ||||||||||
eerhardt marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||
| </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"; | ||
eerhardt marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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; } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.