From 92a7922b0b455d245e7ad92f6bbbae3dc8aff014 Mon Sep 17 00:00:00 2001 From: Andre Lins Date: Wed, 29 Nov 2023 23:49:31 -0300 Subject: [PATCH 01/12] prepare directory --- Aspire.sln | 7 ++++++ .../Aspire.Oracle.EntityFrameworkCore.csproj | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 src/Components/Aspire.Oracle.EntityFrameworkCore/Aspire.Oracle.EntityFrameworkCore.csproj diff --git a/Aspire.sln b/Aspire.sln index 7881faf32d9..7c364d21791 100644 --- a/Aspire.sln +++ b/Aspire.sln @@ -167,6 +167,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.MongoDB.Driver.Tests EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject.LaunchSettings", "tests\testproject\TestProject.LaunchSettings\TestProject.LaunchSettings.csproj", "{A734177E-213B-4D68-98A4-6F5C00234053}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Oracle.EntityFrameworkCore", "src\Components\Aspire.Oracle.EntityFrameworkCore\Aspire.Oracle.EntityFrameworkCore.csproj", "{A778F29A-6C40-4C53-A793-F23F20679ADE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -453,6 +455,10 @@ Global {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -531,6 +537,7 @@ Global {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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6DCEDFEC-988E-4CB3-B45B-191EB5086E0C} diff --git a/src/Components/Aspire.Oracle.EntityFrameworkCore/Aspire.Oracle.EntityFrameworkCore.csproj b/src/Components/Aspire.Oracle.EntityFrameworkCore/Aspire.Oracle.EntityFrameworkCore.csproj new file mode 100644 index 00000000000..2ef67fb6465 --- /dev/null +++ b/src/Components/Aspire.Oracle.EntityFrameworkCore/Aspire.Oracle.EntityFrameworkCore.csproj @@ -0,0 +1,23 @@ + + + + $(NetCurrent) + true + $(ComponentEfCorePackageTags) sqlserver sql + A Oracle Database provider for Entity Framework Core that integrates with Aspire, including connection pooling, health check, logging, and telemetry. + + + + + + + + + + + + + + + + From 0f2f3f845e07ed228ca21e5a8314648ff52de167 Mon Sep 17 00:00:00 2001 From: Andre Lins Date: Fri, 8 Dec 2023 13:46:55 -0300 Subject: [PATCH 02/12] Adds Aspire Oracle EntityFrameworkCore Database component. --- Aspire.sln | 21 +-- Directory.Packages.props | 3 +- ...racle.EntityFrameworkCore.Database.csproj} | 1 + .../AspireOracleEFCoreDatabaseExtensions.cs | 124 ++++++++++++++++++ .../ConfigurationSchema.json | 100 ++++++++++++++ ...acleEntityFrameworkCoreDatabaseSettings.cs | 58 ++++++++ .../README.md | 98 ++++++++++++++ src/Components/Aspire_Components_Progress.md | 1 + ....EntityFrameworkCore.Database.Tests.csproj | 17 +++ ...pireOracleEFCoreDatabaseExtensionsTests.cs | 120 +++++++++++++++++ .../ConformanceTests_NoPooling.cs | 22 ++++ ...manceTests_NoPooling_TypeSpecificConfig.cs | 16 +++ .../ConformanceTests_Pooling.cs | 124 ++++++++++++++++++ ...ormanceTests_Pooling_TypeSpecificConfig.cs | 16 +++ 14 files changed, 711 insertions(+), 10 deletions(-) rename src/Components/{Aspire.Oracle.EntityFrameworkCore/Aspire.Oracle.EntityFrameworkCore.csproj => Aspire.Oracle.EntityFrameworkCore.Database/Aspire.Oracle.EntityFrameworkCore.Database.csproj} (94%) create mode 100644 src/Components/Aspire.Oracle.EntityFrameworkCore.Database/AspireOracleEFCoreDatabaseExtensions.cs create mode 100644 src/Components/Aspire.Oracle.EntityFrameworkCore.Database/ConfigurationSchema.json create mode 100644 src/Components/Aspire.Oracle.EntityFrameworkCore.Database/OracleEntityFrameworkCoreDatabaseSettings.cs create mode 100644 src/Components/Aspire.Oracle.EntityFrameworkCore.Database/README.md create mode 100644 tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests.csproj create mode 100644 tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/AspireOracleEFCoreDatabaseExtensionsTests.cs create mode 100644 tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_NoPooling.cs create mode 100644 tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_NoPooling_TypeSpecificConfig.cs create mode 100644 tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_Pooling.cs create mode 100644 tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_Pooling_TypeSpecificConfig.cs diff --git a/Aspire.sln b/Aspire.sln index 7c364d21791..d8536d43856 100644 --- a/Aspire.sln +++ b/Aspire.sln @@ -161,13 +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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Oracle.EntityFrameworkCore", "src\Components\Aspire.Oracle.EntityFrameworkCore\Aspire.Oracle.EntityFrameworkCore.csproj", "{A778F29A-6C40-4C53-A793-F23F20679ADE}" +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 @@ -447,10 +449,6 @@ 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 @@ -459,6 +457,10 @@ Global {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 @@ -533,11 +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} diff --git a/Directory.Packages.props b/Directory.Packages.props index 0254f2f56a6..6c0414c84f5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -77,6 +77,7 @@ + @@ -102,4 +103,4 @@ - + \ No newline at end of file diff --git a/src/Components/Aspire.Oracle.EntityFrameworkCore/Aspire.Oracle.EntityFrameworkCore.csproj b/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/Aspire.Oracle.EntityFrameworkCore.Database.csproj similarity index 94% rename from src/Components/Aspire.Oracle.EntityFrameworkCore/Aspire.Oracle.EntityFrameworkCore.csproj rename to src/Components/Aspire.Oracle.EntityFrameworkCore.Database/Aspire.Oracle.EntityFrameworkCore.Database.csproj index 2ef67fb6465..228014071d4 100644 --- a/src/Components/Aspire.Oracle.EntityFrameworkCore/Aspire.Oracle.EntityFrameworkCore.csproj +++ b/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/Aspire.Oracle.EntityFrameworkCore.Database.csproj @@ -18,6 +18,7 @@ + diff --git a/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/AspireOracleEFCoreDatabaseExtensions.cs b/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/AspireOracleEFCoreDatabaseExtensions.cs new file mode 100644 index 00000000000..39ef325033a --- /dev/null +++ b/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/AspireOracleEFCoreDatabaseExtensions.cs @@ -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; + +/// +/// Extension methods for configuring EntityFrameworkCore DbContext to Oracle database +/// +public static class AspireOracleEFCoreDatabaseExtensions +{ + private const string DefaultConfigSectionName = "Aspire:Oracle:EntityFrameworkCore:Database"; + private const DynamicallyAccessedMemberTypes RequiredByEF = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties; + + /// + /// Registers the given as a service in the services provided by the . + /// Configures the connection pooling, health check, logging and telemetry for the . + /// + /// The that needs to be registered. + /// The to read config from and add services to. + /// A name used to retrieve the connection string from the ConnectionStrings configuration section. + /// An optional delegate that can be used for customizing options. It's invoked after the settings are read from the configuration. + /// An optional delegate to configure the for the context. + /// Reads the configuration from "Aspire:Oracle:EntityFrameworkCore:Database:{typeof(TContext).Name}" config section, or "Aspire:Oracle:EntityFrameworkCore:Database" if former does not exist. + /// Thrown if mandatory is null. + /// Thrown when mandatory is not provided. + public static void AddOracleDatabaseDbContext<[DynamicallyAccessedMembers(RequiredByEF)] TContext>( + this IHostApplicationBuilder builder, + string connectionName, + Action? configureSettings = null, + Action? 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(ConfigureDbContext); + } + else + { + builder.Services.AddDbContext(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()); + } + + 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); + } + } +} diff --git a/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/ConfigurationSchema.json b/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/ConfigurationSchema.json new file mode 100644 index 00000000000..e8f90d283e6 --- /dev/null +++ b/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/ConfigurationSchema.json @@ -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" +} diff --git a/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/OracleEntityFrameworkCoreDatabaseSettings.cs b/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/OracleEntityFrameworkCoreDatabaseSettings.cs new file mode 100644 index 00000000000..3e1938c8fd2 --- /dev/null +++ b/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/OracleEntityFrameworkCoreDatabaseSettings.cs @@ -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; + +/// +/// Provides the client configuration settings for connecting to a Oracle database using EntityFrameworkCore. +/// +public sealed class OracleEntityFrameworkCoreDatabaseSettings +{ + /// + /// The connection string of the Oracle database to connect to. + /// + public string? ConnectionString { get; set; } + + /// + /// Gets or sets a boolean value that indicates whether the db context will be pooled or explicitly created every time it's requested. + /// + public bool DbContextPooling { get; set; } = true; + + /// + /// Gets or sets the maximum number of retry attempts. + /// + /// The default is 6. + /// Set it to 0 to disable the retry mechanism. + /// + /// + public int MaxRetryCount { get; set; } = 6; + + /// + /// Gets or sets a boolean value that indicates whether the database health check is enabled or not. + /// + /// The default value is . + /// + /// + public bool HealthChecks { get; set; } = true; + + /// + /// Gets or sets a boolean value that indicates whether the Open Telemetry tracing is enabled or not. + /// + /// The default value is . + /// + /// + public bool Tracing { get; set; } = true; + + /// + /// Gets or sets a boolean value that indicates whether the Open Telemetry metrics are enabled or not. + /// + /// The default value is . + /// + /// + public bool Metrics { get; set; } = true; + + /// + /// The time in seconds to wait for the command to execute. + /// + public int? Timeout { get; set; } +} diff --git a/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/README.md b/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/README.md new file mode 100644 index 00000000000..ea8ba9ddfc4 --- /dev/null +++ b/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/README.md @@ -0,0 +1,98 @@ +# Aspire.Oracle.EntityFrameworkCore.Database library + +Registers [EntityFrameworkCore](https://learn.microsoft.com/ef/core/) [DbContext](https://learn.microsoft.com/dotnet/api/microsoft.entityframeworkcore.dbcontext) service for connecting Oracle database. Enables connection pooling, health check, logging and telemetry. + +## Getting started + +### Prerequisites + +- Oracle database and connection string for accessing the database. + +### Install the package + +Install the .NET Aspire Oracle EntityFrameworkCore Database library with [NuGet](https://www.nuget.org): + +```dotnetcli +dotnet add package Aspire.Oracle.EntityFrameworkCore.Database +``` + +## Usage example + +In the _Program.cs_ file of your project, call the `AddOracleDatabaseDbContext` extension method to register a `DbContext` for use via the dependency injection container. The method takes a connection name parameter. + +```csharp +builder.AddOracleDatabaseDbContext("orcl"); +``` + +You can then retrieve the `MyDbContext` instance using dependency injection. For example, to retrieve the context from a Web API controller: + +```csharp +private readonly MyDbContext _context; + +public ProductsController(MyDbContext context) +{ + _context = context; +} +``` + +## Configuration + +The .NET Aspire Oracle EntityFrameworkCore Database component provides multiple options to configure the SQL connection based on the requirements and conventions of your project. + +### Use a connection string + +When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddOracleDatabaseDbContext()`: + +```csharp +builder.AddOracleDatabaseDbContext("myConnection"); +``` + +And then the connection string will be retrieved from the `ConnectionStrings` configuration section: + +```json +{ + "ConnectionStrings": { + "myConnection": "Data Source=TORCL;User Id=myUsername;Password=myPassword;" + } +} +``` + +See the [ODP.NET documentation](https://docs.oracle.com/en/database/oracle/oracle-database/21/odpnt/#Oracle%C2%AE-Data-Provider-for-.NET) for more information on how to format this connection string. + +### Use configuration providers + +The .NET Aspire Oracle EntityFrameworkCore Database component supports [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/api/microsoft.extensions.configuration). It loads the `OracleEntityFrameworkCoreDatabaseSettings` from configuration by using the `Aspire:Microsoft:EntityFrameworkCore:OracleDatabase` key. Example `appsettings.json` that configures some of the options: + +```json +{ + "Aspire": { + "Oracle": { + "EntityFrameworkCore": { + "Database": { + "DbContextPooling": true, + "HealthChecks": false, + "Tracing": false, + "Metrics": true + } + } + } + } +} +``` + +### Use inline delegates + +Also you can pass the `Action configureSettings` delegate to set up some or all the options inline, for example to disable health checks from code: + +```csharp + builder.AddOracleDatabaseDbContext("orcl", settings => settings.HealthChecks = false); +``` + +## Additional documentation + +* https://learn.microsoft.com/ef/core/ +* https://github.com/dotnet/aspire/tree/main/src/Components/README.md + +## Feedback & contributing + +https://github.com/dotnet/aspire diff --git a/src/Components/Aspire_Components_Progress.md b/src/Components/Aspire_Components_Progress.md index 6bfd399c7ac..78d8ee14277 100644 --- a/src/Components/Aspire_Components_Progress.md +++ b/src/Components/Aspire_Components_Progress.md @@ -22,6 +22,7 @@ As part of the .NET Aspire November preview, we want to include a set of .NET As | StackExchange.Redis.OutputCaching | ✅ | ✅ | N/A | ✅ | ✅ | ✅ | ❌ | ✅ | | RabbitMQ | ✅ | ✅ | ✅ | ✅ | | | ❌ | ✅ | | MySqlConnector | ✅ | | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Oracle.EntityFrameworkCore.Database | ✅ | | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | Nomenclature used in the table above: diff --git a/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests.csproj b/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests.csproj new file mode 100644 index 00000000000..4568b53998f --- /dev/null +++ b/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests.csproj @@ -0,0 +1,17 @@ + + + + $(NetCurrent) + + + + + + + + + + + + + diff --git a/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/AspireOracleEFCoreDatabaseExtensionsTests.cs b/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/AspireOracleEFCoreDatabaseExtensionsTests.cs new file mode 100644 index 00000000000..0a4ad3bcd42 --- /dev/null +++ b/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/AspireOracleEFCoreDatabaseExtensionsTests.cs @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Components.Common.Tests; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Oracle.EntityFrameworkCore; +using Oracle.EntityFrameworkCore.Infrastructure.Internal; +using Xunit; + +namespace Aspire.Oracle.EntityFrameworkCore.Database.Tests; + +public class AspireOracleEFCoreDatabaseExtensionsTests +{ + private const string ConnectionString = "Data Source=fake"; + + [Fact] + public void ReadsFromConnectionStringsCorrectly() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("ConnectionStrings:orclconnection", ConnectionString) + ]); + + builder.AddOracleDatabaseDbContext("orclconnection"); + + var host = builder.Build(); + var context = host.Services.GetRequiredService(); + + Assert.Equal(ConnectionString, context.Database.GetDbConnection().ConnectionString); + } + + [Fact] + public void ConnectionStringCanBeSetInCode() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("ConnectionStrings:orclconnection", "unused") + ]); + + builder.AddOracleDatabaseDbContext("orclconnection", settings => settings.ConnectionString = ConnectionString); + + var host = builder.Build(); + var context = host.Services.GetRequiredService(); + + var actualConnectionString = context.Database.GetDbConnection().ConnectionString; + Assert.Equal(ConnectionString, actualConnectionString); + // the connection string from config should not be used since code set it explicitly + Assert.DoesNotContain("unused", actualConnectionString); + } + + [Fact] + public void ConnectionNameWinsOverConfigSection() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("Aspire:Oracle:EntityFrameworkCore:Database:ConnectionString", "unused"), + new KeyValuePair("ConnectionStrings:orclconnection", ConnectionString) + ]); + + builder.AddOracleDatabaseDbContext("orclconnection"); + + var host = builder.Build(); + var context = host.Services.GetRequiredService(); + + var actualConnectionString = context.Database.GetDbConnection().ConnectionString; + Assert.Equal(ConnectionString, actualConnectionString); + // the connection string from config should not be used since it was found in ConnectionStrings + Assert.DoesNotContain("unused", actualConnectionString); + } + + [Fact] + public void CanConfigureDbContextOptions() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("ConnectionStrings:orclconnection", ConnectionString), + new KeyValuePair("Aspire:Oracle:EntityFrameworkCore:Database:MaxRetryCount", "304"), + new KeyValuePair("Aspire:Oracle:EntityFrameworkCore:Database:Timeout", "608") + ]); + + builder.AddOracleDatabaseDbContext("orclconnection", configureDbContextOptions: optionsBuilder => + { + optionsBuilder.UseOracle(orclBuilder => + { + orclBuilder.MinBatchSize(123); + }); + }); + + var host = builder.Build(); + var context = host.Services.GetRequiredService(); + +#pragma warning disable EF1001 // Internal EF Core API usage. + + var extension = context.Options.FindExtension(); + Assert.NotNull(extension); + + // ensure the min batch size was respected + Assert.Equal(123, extension.MinBatchSize); + + // ensure the connection string from config was respected + var actualConnectionString = context.Database.GetDbConnection().ConnectionString; + Assert.Equal(ConnectionString, actualConnectionString); + + // ensure the max retry count from config was respected + Assert.NotNull(extension.ExecutionStrategyFactory); + var executionStrategy = extension.ExecutionStrategyFactory(new ExecutionStrategyDependencies(new CurrentDbContext(context), context.Options, null!)); + var retryStrategy = Assert.IsType(executionStrategy); + Assert.Equal(304, retryStrategy.MaxRetryCount); + + // ensure the command timeout from config was respected + Assert.Equal(608, extension.CommandTimeout); + +#pragma warning restore EF1001 // Internal EF Core API usage. + } +} diff --git a/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_NoPooling.cs b/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_NoPooling.cs new file mode 100644 index 00000000000..c4b73c4086b --- /dev/null +++ b/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_NoPooling.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Components.Common.Tests; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Aspire.Oracle.EntityFrameworkCore.Database.Tests; + +public class ConformanceTests_NoPooling : ConformanceTests_Pooling +{ + protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Scoped; + + protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null) + { + builder.AddOracleDatabaseDbContext("orclconnection", settings => + { + settings.DbContextPooling = false; + configure?.Invoke(settings); + }); + } +} diff --git a/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_NoPooling_TypeSpecificConfig.cs b/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_NoPooling_TypeSpecificConfig.cs new file mode 100644 index 00000000000..94215fd3720 --- /dev/null +++ b/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_NoPooling_TypeSpecificConfig.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Components.Common.Tests; +using Microsoft.Extensions.Configuration; + +namespace Aspire.Oracle.EntityFrameworkCore.Database.Tests; + +public class ConformanceTests_NoPooling_TypeSpecificConfig : ConformanceTests_NoPooling +{ + protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null) + => configuration.AddInMemoryCollection(new KeyValuePair[1] + { + new($"Aspire:Oracle:EntityFrameworkCore:Database:{typeof(TestDbContext).Name}:ConnectionString", ConnectionString) + }); +} diff --git a/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_Pooling.cs b/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_Pooling.cs new file mode 100644 index 00000000000..827ed595509 --- /dev/null +++ b/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_Pooling.cs @@ -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 Aspire.Components.Common.Tests; +using Aspire.Components.ConformanceTests; +using Microsoft.DotNet.RemoteExecutor; +using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Aspire.Oracle.EntityFrameworkCore.Database.Tests; + +public class ConformanceTests_Pooling : ConformanceTests +{ + protected const string ConnectionString = "Data Source=fake;"; + + protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Singleton; + + // https://github.com/open-telemetry/opentelemetry-dotnet-contrib/blob/cb5b2193ef9cacc0b9ef699e085022577551bf85/src/OpenTelemetry.Instrumentation.EntityFrameworkCore/Implementation/EntityFrameworkDiagnosticListener.cs#L38 + protected override string ActivitySourceName => "OpenTelemetry.Instrumentation.EntityFrameworkCore"; + + protected override string[] RequiredLogCategories => new string[] + { + "Microsoft.EntityFrameworkCore.Infrastructure", + "Microsoft.EntityFrameworkCore.ChangeTracking", + "Microsoft.EntityFrameworkCore.Infrastructure", + "Microsoft.EntityFrameworkCore.Database.Command", + "Microsoft.EntityFrameworkCore.Query", + "Microsoft.EntityFrameworkCore.Database.Transaction", + "Microsoft.EntityFrameworkCore.Database.Connection", + "Microsoft.EntityFrameworkCore.Model", + "Microsoft.EntityFrameworkCore.Model.Validation", + "Microsoft.EntityFrameworkCore.Update", + "Microsoft.EntityFrameworkCore.Migrations" + }; + + protected override string JsonSchemaPath => "src/Components/Aspire.Oracle.EntityFrameworkCore.Database/ConfigurationSchema.json"; + + protected override string ValidJsonConfig => """ + { + "Aspire": { + "Oracle": { + "EntityFrameworkCore": { + "Database": { + "ConnectionString": "YOUR_CONNECTION_STRING", + "HealthChecks": false, + "DbContextPooling": true, + "Tracing": true, + "Metrics": true + } + } + } + } + } + """; + + protected override (string json, string error)[] InvalidJsonToErrorMessage => new[] + { + ("""{"Aspire": { "Oracle": { "EntityFrameworkCore":{ "Database": { "MaxRetryCount": "5"}}}}}""", "Value is \"string\" but should be \"integer\""), + ("""{"Aspire": { "Oracle": { "EntityFrameworkCore":{ "Database": { "HealthChecks": "false"}}}}}""", "Value is \"string\" but should be \"boolean\""), + ("""{"Aspire": { "Oracle": { "EntityFrameworkCore":{ "Database": { "ConnectionString": "", "DbContextPooling": "Yes"}}}}}""", "Value is \"string\" but should be \"boolean\"") + }; + + protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null) + => configuration.AddInMemoryCollection(new KeyValuePair[1] + { + new KeyValuePair("Aspire:Oracle:EntityFrameworkCore:Database:ConnectionString", ConnectionString) + }); + + protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null) + => builder.AddOracleDatabaseDbContext("orclconnection", configure); + + protected override void SetHealthCheck(OracleEntityFrameworkCoreDatabaseSettings options, bool enabled) + => options.HealthChecks = enabled; + + protected override void SetTracing(OracleEntityFrameworkCoreDatabaseSettings options, bool enabled) + => options.Tracing = enabled; + + protected override void SetMetrics(OracleEntityFrameworkCoreDatabaseSettings options, bool enabled) + => options.Metrics = enabled; + + protected override void TriggerActivity(TestDbContext service) + { + if (service.Database.CanConnect()) + { + service.Database.EnsureCreated(); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "Required to verify pooling without touching DB")] + public void DbContextPoolingRegistersIDbContextPool(bool enabled) + { + using IHost host = CreateHostWithComponent(options => options.DbContextPooling = enabled); + + IDbContextPool? pool = host.Services.GetService>(); + + Assert.Equal(enabled, pool is not null); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DbContextCanBeAlwaysResolved(bool enabled) + { + using IHost host = CreateHostWithComponent(options => options.DbContextPooling = enabled); + + TestDbContext? dbContext = host.Services.GetService(); + + Assert.NotNull(dbContext); + } + + [ConditionalFact] + public void TracingEnablesTheRightActivitySource() + { + SkipIfCanNotConnectToServer(); + + RemoteExecutor.Invoke(() => ActivitySourceTest(key: null)).Dispose(); + } +} diff --git a/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_Pooling_TypeSpecificConfig.cs b/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_Pooling_TypeSpecificConfig.cs new file mode 100644 index 00000000000..f3fb8ccd987 --- /dev/null +++ b/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_Pooling_TypeSpecificConfig.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Components.Common.Tests; +using Microsoft.Extensions.Configuration; + +namespace Aspire.Oracle.EntityFrameworkCore.Database.Tests; + +public class ConformanceTests_Pooling_TypeSpecificConfig : ConformanceTests_Pooling +{ + protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null) + => configuration.AddInMemoryCollection(new KeyValuePair[1] + { + new($"Aspire:Oracle:EntityFrameworkCore:Database:{typeof(TestDbContext).Name}:ConnectionString", ConnectionString) + }); +} From 12ee3965d23d88ed2f6c23a6c995751f8afbbbc3 Mon Sep 17 00:00:00 2001 From: Andre Lins Date: Mon, 11 Dec 2023 11:27:55 -0300 Subject: [PATCH 03/12] AppModel apis --- .../Oracle/IOracleDatabaseResource.cs | 11 +++ .../Oracle/OracleDatabaseBuilderExtensions.cs | 81 +++++++++++++++++++ .../OracleDatabaseConnectionResource.cs | 20 +++++ .../Oracle/OracleDatabaseContainerResource.cs | 31 +++++++ .../Oracle/OracleDatabaseResource.cs | 30 +++++++ 5 files changed, 173 insertions(+) create mode 100644 src/Aspire.Hosting/Oracle/IOracleDatabaseResource.cs create mode 100644 src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs create mode 100644 src/Aspire.Hosting/Oracle/OracleDatabaseConnectionResource.cs create mode 100644 src/Aspire.Hosting/Oracle/OracleDatabaseContainerResource.cs create mode 100644 src/Aspire.Hosting/Oracle/OracleDatabaseResource.cs diff --git a/src/Aspire.Hosting/Oracle/IOracleDatabaseResource.cs b/src/Aspire.Hosting/Oracle/IOracleDatabaseResource.cs new file mode 100644 index 00000000000..1dedeedc73a --- /dev/null +++ b/src/Aspire.Hosting/Oracle/IOracleDatabaseResource.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Represents a Oracle Database resource that requires a connection string. +/// +public interface IOracleDatabaseResource : IResourceWithConnectionString +{ +} diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs new file mode 100644 index 00000000000..9a244432a24 --- /dev/null +++ b/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Sockets; +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Publishing; + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for adding Oracle Database resources to an . +/// +public static class OracleDatabaseBuilderExtensions +{ + private const string PasswordEnvVarName = "ORACLE_DATABASE_PASSWORD"; + + /// + /// Adds a Oracle Database container to the application model. The default image is "database/free" and the tag is "latest". + /// + /// The . + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. + /// The host port for Oracle Database. + /// The password for the Oracle Database container. Defaults to a random password. + /// A reference to the . + public static IResourceBuilder AddOracleDatabaseContainer(this IDistributedApplicationBuilder builder, string name, int? port = null, string? password = null) + { + password = password ?? Guid.NewGuid().ToString("N"); + var oracleDatabaseContainer = new OracleDatabaseContainerResource(name, password); + return builder.AddResource(oracleDatabaseContainer) + .WithAnnotation(new ManifestPublishingCallbackAnnotation(WriteOracleDatabaseContainerToManifest)) + .WithAnnotation(new ServiceBindingAnnotation(ProtocolType.Tcp, port: port, containerPort: 1521)) + .WithAnnotation(new ContainerImageAnnotation { Image = "database/free", Tag = "latest", Registry = "container-registry.oracle.com" }) + .WithEnvironment(PasswordEnvVarName, () => oracleDatabaseContainer.Password); + } + + /// + /// Adds a Oracle Database connection to the application model. Connection strings can also be read from the connection string section of the configuration using the name of the resource. + /// + /// The . + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. + /// The Oracle Database connection string (optional). + /// A reference to the . + public static IResourceBuilder AddOracleDatabaseConnection(this IDistributedApplicationBuilder builder, string name, string? connectionString = null) + { + var oracleDatabaseConnection = new OracleDatabaseConnectionResource(name, connectionString); + + return builder.AddResource(oracleDatabaseConnection) + .WithAnnotation(new ManifestPublishingCallbackAnnotation((context) => WriteOracleDatabaseConnectionToManifest(context, oracleDatabaseConnection))); + } + + /// + /// Adds a Oracle Database database to the application model. + /// + /// The Oracle Database server resource builder. + /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. + /// A reference to the . + public static IResourceBuilder AddDatabase(this IResourceBuilder builder, string name) + { + var oracleDatabase = new OracleDatabaseResource(name, builder.Resource); + return builder.ApplicationBuilder.AddResource(oracleDatabase) + .WithAnnotation(new ManifestPublishingCallbackAnnotation( + (json) => WriteOracleDatabaseToManifest(json, oracleDatabase))); + } + + private static void WriteOracleDatabaseConnectionToManifest(ManifestPublishingContext context, OracleDatabaseConnectionResource oracleDatabaseConnection) + { + context.Writer.WriteString("type", "oracle.connection.v0"); + context.Writer.WriteString("connectionString", oracleDatabaseConnection.GetConnectionString()); + } + + private static void WriteOracleDatabaseContainerToManifest(ManifestPublishingContext context) + { + context.Writer.WriteString("type", "oracle.server.v0"); + } + + private static void WriteOracleDatabaseToManifest(ManifestPublishingContext context, OracleDatabaseResource oracleDatabase) + { + context.Writer.WriteString("type", "oracle.database.v0"); + context.Writer.WriteString("parent", oracleDatabase.Parent.Name); + } +} diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseConnectionResource.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseConnectionResource.cs new file mode 100644 index 00000000000..12bce173052 --- /dev/null +++ b/src/Aspire.Hosting/Oracle/OracleDatabaseConnectionResource.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// A resource that represents a Oracle Database connection. +/// +/// The name of the resource. +/// The Oracle Database connection string. +public class OracleDatabaseConnectionResource(string name, string? connectionString) : Resource(name), IOracleDatabaseResource +{ + private readonly string? _connectionString = connectionString; + + /// + /// Gets the connection string for the Oracle Database server. + /// + /// The specified connection string. + public string? GetConnectionString() => _connectionString; +} diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseContainerResource.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseContainerResource.cs new file mode 100644 index 00000000000..c54ba95e9a4 --- /dev/null +++ b/src/Aspire.Hosting/Oracle/OracleDatabaseContainerResource.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// A resource that represents a Oracle Database container. +/// +/// The name of the resource. +/// The Oracle Database server password. +public class OracleDatabaseContainerResource(string name, string password) : ContainerResource(name), IOracleDatabaseResource +{ + public string Password { get; } = password; + + /// + /// Gets the connection string for the Oracle Database server. + /// + /// A connection string for the Oracle Database server in the form "user id=system;password=password;data source=localhost:port". + public string? GetConnectionString() + { + if (!this.TryGetAllocatedEndPoints(out var allocatedEndpoints)) + { + throw new DistributedApplicationException("Expected allocated endpoints!"); + } + + var allocatedEndpoint = allocatedEndpoints.Single(); // We should only have one endpoint for Oracle Database. + + var connectionString = $"user id=system;password={Password};data source={allocatedEndpoint.Address}:{allocatedEndpoint.Port}"; + return connectionString; + } +} diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseResource.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseResource.cs new file mode 100644 index 00000000000..a258de32d77 --- /dev/null +++ b/src/Aspire.Hosting/Oracle/OracleDatabaseResource.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// A resource that represents a Oracle Database database. This is a child resource of a . +/// +/// The name of the resource. +/// The Oracle Database server resource associated with this database. +public class OracleDatabaseResource(string name, OracleDatabaseContainerResource oracleContainer) : Resource(name), IOracleDatabaseResource, IResourceWithParent +{ + public OracleDatabaseContainerResource Parent { get; } = oracleContainer; + + /// + /// Gets the connection string for the Oracle Database. + /// + /// A connection string for the Oracle Database. + public string? GetConnectionString() + { + if (Parent.GetConnectionString() is { } connectionString) + { + return $"{connectionString}/{Name}"; + } + else + { + throw new DistributedApplicationException("Parent resource connection string was null."); + } + } +} From 773b457fe60ff4e40051e2e30d1b60dbe846baf1 Mon Sep 17 00:00:00 2001 From: Andre Lins Date: Wed, 13 Dec 2023 21:23:35 -0300 Subject: [PATCH 04/12] appmodel apis --- .../Oracle/IOracleDatabaseResource.cs | 2 +- .../Oracle/OracleDatabaseBuilderExtensions.cs | 64 +++++++++++++------ .../OracleDatabaseConnectionResource.cs | 20 ------ .../Oracle/OracleDatabaseContainerResource.cs | 2 +- .../Oracle/OracleDatabaseResource.cs | 6 +- .../Oracle/OracleDatabaseServerResource.cs | 33 ++++++++++ 6 files changed, 82 insertions(+), 45 deletions(-) delete mode 100644 src/Aspire.Hosting/Oracle/OracleDatabaseConnectionResource.cs create mode 100644 src/Aspire.Hosting/Oracle/OracleDatabaseServerResource.cs diff --git a/src/Aspire.Hosting/Oracle/IOracleDatabaseResource.cs b/src/Aspire.Hosting/Oracle/IOracleDatabaseResource.cs index 1dedeedc73a..d5b63d1d050 100644 --- a/src/Aspire.Hosting/Oracle/IOracleDatabaseResource.cs +++ b/src/Aspire.Hosting/Oracle/IOracleDatabaseResource.cs @@ -6,6 +6,6 @@ namespace Aspire.Hosting.ApplicationModel; /// /// Represents a Oracle Database resource that requires a connection string. /// -public interface IOracleDatabaseResource : IResourceWithConnectionString +public interface IOracleDatabaseParentResource : IResourceWithConnectionString, IResourceWithEnvironment { } diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs index 9a244432a24..54a61a94aa6 100644 --- a/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs +++ b/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs @@ -12,7 +12,7 @@ namespace Aspire.Hosting; /// public static class OracleDatabaseBuilderExtensions { - private const string PasswordEnvVarName = "ORACLE_DATABASE_PASSWORD"; + private const string PasswordEnvVarName = "ORACLE_PWD"; /// /// Adds a Oracle Database container to the application model. The default image is "database/free" and the tag is "latest". @@ -27,25 +27,37 @@ public static IResourceBuilder AddOracleDatabas password = password ?? Guid.NewGuid().ToString("N"); var oracleDatabaseContainer = new OracleDatabaseContainerResource(name, password); return builder.AddResource(oracleDatabaseContainer) - .WithAnnotation(new ManifestPublishingCallbackAnnotation(WriteOracleDatabaseContainerToManifest)) + .WithManifestPublishingCallback(context => WriteOracleDatabaseContainerResourceToManifest(context, oracleDatabaseContainer)) .WithAnnotation(new ServiceBindingAnnotation(ProtocolType.Tcp, port: port, containerPort: 1521)) .WithAnnotation(new ContainerImageAnnotation { Image = "database/free", Tag = "latest", Registry = "container-registry.oracle.com" }) - .WithEnvironment(PasswordEnvVarName, () => oracleDatabaseContainer.Password); + .WithEnvironment(context => + { + if (context.PublisherName == "manifest") + { + context.EnvironmentVariables.Add(PasswordEnvVarName, $"{{{oracleDatabaseContainer.Name}.inputs.password}}"); + } + else + { + context.EnvironmentVariables.Add(PasswordEnvVarName, oracleDatabaseContainer.Password); + } + }); } /// - /// Adds a Oracle Database connection to the application model. Connection strings can also be read from the connection string section of the configuration using the name of the resource. + /// Adds a Oracle Database resource to the application model. A container is used for local development. /// /// The . /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. - /// The Oracle Database connection string (optional). - /// A reference to the . - public static IResourceBuilder AddOracleDatabaseConnection(this IDistributedApplicationBuilder builder, string name, string? connectionString = null) + /// A reference to the . + public static IResourceBuilder AddOracleDatabase(this IDistributedApplicationBuilder builder, string name) { - var oracleDatabaseConnection = new OracleDatabaseConnectionResource(name, connectionString); - - return builder.AddResource(oracleDatabaseConnection) - .WithAnnotation(new ManifestPublishingCallbackAnnotation((context) => WriteOracleDatabaseConnectionToManifest(context, oracleDatabaseConnection))); + var password = "123"; // Guid.NewGuid().ToString("N"); + var oracleDatabaseServer = new OracleDatabaseServerResource(name, password); + return builder.AddResource(oracleDatabaseServer) + .WithManifestPublishingCallback(WriteOracleDatabaseContainerToManifest) + .WithAnnotation(new ServiceBindingAnnotation(ProtocolType.Tcp, containerPort: 1521)) + .WithAnnotation(new ContainerImageAnnotation { Image = "database/free", Tag = "latest", Registry = "container-registry.oracle.com" }) + .WithEnvironment(PasswordEnvVarName, () => oracleDatabaseServer.Password); } /// @@ -54,18 +66,11 @@ public static IResourceBuilder AddOracleDataba /// The Oracle Database server resource builder. /// The name of the resource. This name will be used as the connection string name when referenced in a dependency. /// A reference to the . - public static IResourceBuilder AddDatabase(this IResourceBuilder builder, string name) + public static IResourceBuilder AddDatabase(this IResourceBuilder builder, string name) { var oracleDatabase = new OracleDatabaseResource(name, builder.Resource); return builder.ApplicationBuilder.AddResource(oracleDatabase) - .WithAnnotation(new ManifestPublishingCallbackAnnotation( - (json) => WriteOracleDatabaseToManifest(json, oracleDatabase))); - } - - private static void WriteOracleDatabaseConnectionToManifest(ManifestPublishingContext context, OracleDatabaseConnectionResource oracleDatabaseConnection) - { - context.Writer.WriteString("type", "oracle.connection.v0"); - context.Writer.WriteString("connectionString", oracleDatabaseConnection.GetConnectionString()); + .WithManifestPublishingCallback(context => WriteOracleDatabaseToManifest(context, oracleDatabase)); } private static void WriteOracleDatabaseContainerToManifest(ManifestPublishingContext context) @@ -78,4 +83,23 @@ private static void WriteOracleDatabaseToManifest(ManifestPublishingContext cont context.Writer.WriteString("type", "oracle.database.v0"); context.Writer.WriteString("parent", oracleDatabase.Parent.Name); } + + private static void WriteOracleDatabaseContainerResourceToManifest(ManifestPublishingContext context, OracleDatabaseContainerResource resource) + { + context.WriteContainer(resource); + context.Writer.WriteString( // "connectionString": "...", + "connectionString", + $"user id=system;password={{{resource.Name}.inputs.password}};data source={{{resource.Name}.bindings.tcp.host}}:{{{resource.Name}.bindings.tcp.port}};"); + context.Writer.WriteStartObject("inputs"); // "inputs": { + context.Writer.WriteStartObject("password"); // "password": { + context.Writer.WriteString("type", "string"); // "type": "string", + context.Writer.WriteBoolean("secret", true); // "secret": true, + context.Writer.WriteStartObject("default"); // "default": { + context.Writer.WriteStartObject("generate"); // "generate": { + context.Writer.WriteNumber("minLength", 10); // "minLength": 10, + context.Writer.WriteEndObject(); // } + context.Writer.WriteEndObject(); // } + context.Writer.WriteEndObject(); // } + context.Writer.WriteEndObject(); // } + } } diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseConnectionResource.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseConnectionResource.cs deleted file mode 100644 index 12bce173052..00000000000 --- a/src/Aspire.Hosting/Oracle/OracleDatabaseConnectionResource.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Aspire.Hosting.ApplicationModel; - -/// -/// A resource that represents a Oracle Database connection. -/// -/// The name of the resource. -/// The Oracle Database connection string. -public class OracleDatabaseConnectionResource(string name, string? connectionString) : Resource(name), IOracleDatabaseResource -{ - private readonly string? _connectionString = connectionString; - - /// - /// Gets the connection string for the Oracle Database server. - /// - /// The specified connection string. - public string? GetConnectionString() => _connectionString; -} diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseContainerResource.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseContainerResource.cs index c54ba95e9a4..76e697219e7 100644 --- a/src/Aspire.Hosting/Oracle/OracleDatabaseContainerResource.cs +++ b/src/Aspire.Hosting/Oracle/OracleDatabaseContainerResource.cs @@ -8,7 +8,7 @@ namespace Aspire.Hosting.ApplicationModel; /// /// The name of the resource. /// The Oracle Database server password. -public class OracleDatabaseContainerResource(string name, string password) : ContainerResource(name), IOracleDatabaseResource +public class OracleDatabaseContainerResource(string name, string password) : ContainerResource(name), IOracleDatabaseParentResource { public string Password { get; } = password; diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseResource.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseResource.cs index a258de32d77..0f9e1dfa271 100644 --- a/src/Aspire.Hosting/Oracle/OracleDatabaseResource.cs +++ b/src/Aspire.Hosting/Oracle/OracleDatabaseResource.cs @@ -7,10 +7,10 @@ namespace Aspire.Hosting.ApplicationModel; /// A resource that represents a Oracle Database database. This is a child resource of a . /// /// The name of the resource. -/// The Oracle Database server resource associated with this database. -public class OracleDatabaseResource(string name, OracleDatabaseContainerResource oracleContainer) : Resource(name), IOracleDatabaseResource, IResourceWithParent +/// The Oracle Database parent resource associated with this database. +public class OracleDatabaseResource(string name, IOracleDatabaseParentResource oracleParentResource) : Resource(name), IResourceWithParent, IResourceWithConnectionString { - public OracleDatabaseContainerResource Parent { get; } = oracleContainer; + public IOracleDatabaseParentResource Parent { get; } = oracleParentResource; /// /// Gets the connection string for the Oracle Database. diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseServerResource.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseServerResource.cs new file mode 100644 index 00000000000..ecc9ca40d29 --- /dev/null +++ b/src/Aspire.Hosting/Oracle/OracleDatabaseServerResource.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.Utils; + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// A resource that represents a Oracle Database container. +/// +/// The name of the resource. +/// The Oracle Database server password. +public class OracleDatabaseServerResource(string name, string password) : Resource(name), IOracleDatabaseParentResource +{ + public string Password { get; } = password; + + /// + /// Gets the connection string for the Oracle Database server. + /// + /// A connection string for the Oracle Database server in the form "user id=system;password=password;data source=host:port". + public string? GetConnectionString() + { + if (!this.TryGetAllocatedEndPoints(out var allocatedEndpoints)) + { + throw new DistributedApplicationException("Expected allocated endpoints!"); + } + + var allocatedEndpoint = allocatedEndpoints.Single(); // We should only have one endpoint for Oracle. + + var connectionString = $"user id=system;password={PasswordUtil.EscapePassword(Password)};data source={allocatedEndpoint.Address}:{allocatedEndpoint.Port}"; + return connectionString; + } +} From 06d7a4c7dc06062d9c4303871edf3eb1a049ab7d Mon Sep 17 00:00:00 2001 From: Andre Lins Date: Wed, 13 Dec 2023 21:23:58 -0300 Subject: [PATCH 05/12] tests for oracle hosting --- .../Oracle/OracleContainerResourceTests.cs | 31 +++++++++++++++++++ .../TestProject.IntegrationServiceA.csproj | 1 + 2 files changed, 32 insertions(+) create mode 100644 tests/Aspire.Hosting.Tests/Oracle/OracleContainerResourceTests.cs diff --git a/tests/Aspire.Hosting.Tests/Oracle/OracleContainerResourceTests.cs b/tests/Aspire.Hosting.Tests/Oracle/OracleContainerResourceTests.cs new file mode 100644 index 00000000000..f8a679291b5 --- /dev/null +++ b/tests/Aspire.Hosting.Tests/Oracle/OracleContainerResourceTests.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using Aspire.Hosting.Utils; +using Oracle.ManagedDataAccess.Client; +using Xunit; + +namespace Aspire.Hosting.Tests.Oracle; + +public class OracleContainerResourceTests +{ + [Theory()] + [InlineData(["password", "user id=system;password=\"password\";data source=myserver:1521;"])] + [InlineData(["mypasswordwitha\"inthemiddle", "user id=system;password=\"mypasswordwitha\"\"inthemiddle\";data source=myserver:1521;"])] + [InlineData(["mypasswordwitha\"attheend\"", "user id=system;password=\"mypasswordwitha\"\"attheend\"\"\";data source=myserver:1521;"])] + [InlineData(["\"mypasswordwitha\"atthestart", "user id=system;password=\"\"\"mypasswordwitha\"\"atthestart\";data source=myserver:1521;"])] + [InlineData(["mypasswordwitha'inthemiddle", "user id=system;password=\"mypasswordwitha'inthemiddle\";data source=myserver:1521;"])] + [InlineData(["mypasswordwitha'attheend'", "user id=system;password=\"mypasswordwitha'attheend'\";data source=myserver:1521;"])] + [InlineData(["'mypasswordwitha'atthestart", "user id=system;password=\"'mypasswordwitha'atthestart\";data source=myserver:1521;"])] + public void TestEscapeSequencesForPassword(string password, string expectedConnectionString) + { + var connectionStringTemplate = "user id=system;password=\"{0}\";data source=myserver:1521;"; + var escapedPassword = PasswordUtil.EscapePassword(password); + var actualConnectionString = string.Format(CultureInfo.InvariantCulture, connectionStringTemplate, escapedPassword); + + var builder = new OracleConnectionStringBuilder(actualConnectionString); + Assert.Equal(password, builder.Password); + Assert.Equal(expectedConnectionString, actualConnectionString); + } +} diff --git a/tests/testproject/TestProject.IntegrationServiceA/TestProject.IntegrationServiceA.csproj b/tests/testproject/TestProject.IntegrationServiceA/TestProject.IntegrationServiceA.csproj index 45a3272abcc..95446906908 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/TestProject.IntegrationServiceA.csproj +++ b/tests/testproject/TestProject.IntegrationServiceA/TestProject.IntegrationServiceA.csproj @@ -14,6 +14,7 @@ + From 4db7868963e33f516df7245b5ef95ecef4f5488d Mon Sep 17 00:00:00 2001 From: Andre Lins Date: Fri, 15 Dec 2023 11:40:39 -0300 Subject: [PATCH 06/12] adjusting to reviews --- Aspire.sln | 4 +- .../Oracle/OracleDatabaseBuilderExtensions.cs | 2 +- .../ConfigurationSchema.json | 100 --------- .../Aspire.Oracle.EntityFrameworkCore.csproj} | 4 +- .../AspireOracleEFCoreExtensions.cs} | 17 +- .../ConfigurationSchema.json | 95 ++++++++ .../OracleEntityFrameworkCoreSettings.cs} | 4 +- .../README.md | 11 + src/Components/Aspire_Components_Progress.md | 2 +- src/Components/Telemetry.md | 27 +++ .../AddOracleDatabaseTests.cs | 206 ++++++++++++++++++ ...e.Oracle.EntityFrameworkCore.Tests.csproj} | 2 +- ...pireOracleEFCoreDatabaseExtensionsTests.cs | 8 +- .../ConformanceTests_NoPooling.cs | 4 +- ...manceTests_NoPooling_TypeSpecificConfig.cs | 4 +- .../ConformanceTests_Pooling.cs | 34 ++- ...ormanceTests_Pooling_TypeSpecificConfig.cs | 4 +- 17 files changed, 383 insertions(+), 145 deletions(-) delete mode 100644 src/Components/Aspire.Oracle.EntityFrameworkCore.Database/ConfigurationSchema.json rename src/Components/{Aspire.Oracle.EntityFrameworkCore.Database/Aspire.Oracle.EntityFrameworkCore.Database.csproj => Aspire.Oracle.EntityFrameworkCore/Aspire.Oracle.EntityFrameworkCore.csproj} (77%) rename src/Components/{Aspire.Oracle.EntityFrameworkCore.Database/AspireOracleEFCoreDatabaseExtensions.cs => Aspire.Oracle.EntityFrameworkCore/AspireOracleEFCoreExtensions.cs} (88%) create mode 100644 src/Components/Aspire.Oracle.EntityFrameworkCore/ConfigurationSchema.json rename src/Components/{Aspire.Oracle.EntityFrameworkCore.Database/OracleEntityFrameworkCoreDatabaseSettings.cs => Aspire.Oracle.EntityFrameworkCore/OracleEntityFrameworkCoreSettings.cs} (94%) rename src/Components/{Aspire.Oracle.EntityFrameworkCore.Database => Aspire.Oracle.EntityFrameworkCore}/README.md (90%) create mode 100644 tests/Aspire.Hosting.Tests/AddOracleDatabaseTests.cs rename tests/{Aspire.Oracle.EntityFrameworkCore.Database.Tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests.csproj => Aspire.Oracle.EntityFrameworkCore.Tests/Aspire.Oracle.EntityFrameworkCore.Tests.csproj} (87%) rename tests/{Aspire.Oracle.EntityFrameworkCore.Database.Tests => Aspire.Oracle.EntityFrameworkCore.Tests}/AspireOracleEFCoreDatabaseExtensionsTests.cs (95%) rename tests/{Aspire.Oracle.EntityFrameworkCore.Database.Tests => Aspire.Oracle.EntityFrameworkCore.Tests}/ConformanceTests_NoPooling.cs (80%) rename tests/{Aspire.Oracle.EntityFrameworkCore.Database.Tests => Aspire.Oracle.EntityFrameworkCore.Tests}/ConformanceTests_NoPooling_TypeSpecificConfig.cs (73%) rename tests/{Aspire.Oracle.EntityFrameworkCore.Database.Tests => Aspire.Oracle.EntityFrameworkCore.Tests}/ConformanceTests_Pooling.cs (80%) rename tests/{Aspire.Oracle.EntityFrameworkCore.Database.Tests => Aspire.Oracle.EntityFrameworkCore.Tests}/ConformanceTests_Pooling_TypeSpecificConfig.cs (73%) diff --git a/Aspire.sln b/Aspire.sln index df9e2063b4c..e8923f23016 100644 --- a/Aspire.sln +++ b/Aspire.sln @@ -167,9 +167,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.MongoDB.Driver.Tests EndProject 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}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Oracle.EntityFrameworkCore", "src\Components\Aspire.Oracle.EntityFrameworkCore\Aspire.Oracle.EntityFrameworkCore.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}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Oracle.EntityFrameworkCore.Tests", "tests\Aspire.Oracle.EntityFrameworkCore.Tests\Aspire.Oracle.EntityFrameworkCore.Tests.csproj", "{A331C123-35A5-4E81-9999-354159821374}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs index 54a61a94aa6..932575792b0 100644 --- a/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs +++ b/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs @@ -51,7 +51,7 @@ public static IResourceBuilder AddOracleDatabas /// A reference to the . public static IResourceBuilder AddOracleDatabase(this IDistributedApplicationBuilder builder, string name) { - var password = "123"; // Guid.NewGuid().ToString("N"); + var password = Guid.NewGuid().ToString("N"); var oracleDatabaseServer = new OracleDatabaseServerResource(name, password); return builder.AddResource(oracleDatabaseServer) .WithManifestPublishingCallback(WriteOracleDatabaseContainerToManifest) diff --git a/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/ConfigurationSchema.json b/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/ConfigurationSchema.json deleted file mode 100644 index e8f90d283e6..00000000000 --- a/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/ConfigurationSchema.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "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" -} diff --git a/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/Aspire.Oracle.EntityFrameworkCore.Database.csproj b/src/Components/Aspire.Oracle.EntityFrameworkCore/Aspire.Oracle.EntityFrameworkCore.csproj similarity index 77% rename from src/Components/Aspire.Oracle.EntityFrameworkCore.Database/Aspire.Oracle.EntityFrameworkCore.Database.csproj rename to src/Components/Aspire.Oracle.EntityFrameworkCore/Aspire.Oracle.EntityFrameworkCore.csproj index 228014071d4..7f9465d64c9 100644 --- a/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/Aspire.Oracle.EntityFrameworkCore.Database.csproj +++ b/src/Components/Aspire.Oracle.EntityFrameworkCore/Aspire.Oracle.EntityFrameworkCore.csproj @@ -3,8 +3,8 @@ $(NetCurrent) true - $(ComponentEfCorePackageTags) sqlserver sql - A Oracle Database provider for Entity Framework Core that integrates with Aspire, including connection pooling, health check, logging, and telemetry. + $(ComponentEfCorePackageTags) oracle sql + An Oracle Database provider for Entity Framework Core that integrates with Aspire, including connection pooling, health check, logging, and telemetry. diff --git a/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/AspireOracleEFCoreDatabaseExtensions.cs b/src/Components/Aspire.Oracle.EntityFrameworkCore/AspireOracleEFCoreExtensions.cs similarity index 88% rename from src/Components/Aspire.Oracle.EntityFrameworkCore.Database/AspireOracleEFCoreDatabaseExtensions.cs rename to src/Components/Aspire.Oracle.EntityFrameworkCore/AspireOracleEFCoreExtensions.cs index 39ef325033a..1a30234899d 100644 --- a/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/AspireOracleEFCoreDatabaseExtensions.cs +++ b/src/Components/Aspire.Oracle.EntityFrameworkCore/AspireOracleEFCoreExtensions.cs @@ -3,7 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Aspire; -using Aspire.Oracle.EntityFrameworkCore.Database; +using Aspire.Oracle.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -16,9 +16,9 @@ namespace Microsoft.Extensions.Hosting; /// /// Extension methods for configuring EntityFrameworkCore DbContext to Oracle database /// -public static class AspireOracleEFCoreDatabaseExtensions +public static class AspireOracleEFCoreExtensions { - private const string DefaultConfigSectionName = "Aspire:Oracle:EntityFrameworkCore:Database"; + private const string DefaultConfigSectionName = "Aspire:Oracle:EntityFrameworkCore"; private const DynamicallyAccessedMemberTypes RequiredByEF = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties; /// @@ -30,18 +30,18 @@ public static class AspireOracleEFCoreDatabaseExtensions /// A name used to retrieve the connection string from the ConnectionStrings configuration section. /// An optional delegate that can be used for customizing options. It's invoked after the settings are read from the configuration. /// An optional delegate to configure the for the context. - /// Reads the configuration from "Aspire:Oracle:EntityFrameworkCore:Database:{typeof(TContext).Name}" config section, or "Aspire:Oracle:EntityFrameworkCore:Database" if former does not exist. + /// Reads the configuration from "Aspire:Oracle:EntityFrameworkCore:{typeof(TContext).Name}" config section, or "Aspire:Oracle:EntityFrameworkCore" if former does not exist. /// Thrown if mandatory is null. - /// Thrown when mandatory is not provided. + /// Thrown when mandatory is not provided. public static void AddOracleDatabaseDbContext<[DynamicallyAccessedMembers(RequiredByEF)] TContext>( this IHostApplicationBuilder builder, string connectionName, - Action? configureSettings = null, + Action? configureSettings = null, Action? configureDbContextOptions = null) where TContext : DbContext { ArgumentNullException.ThrowIfNull(builder); - OracleEntityFrameworkCoreDatabaseSettings settings = new(); + OracleEntityFrameworkCoreSettings settings = new(); var typeSpecificSectionName = $"{DefaultConfigSectionName}:{typeof(TContext).Name}"; var typeSpecificConfigurationSection = builder.Configuration.GetSection(typeSpecificSectionName); if (typeSpecificConfigurationSection.Exists()) // https://github.com/dotnet/runtime/issues/91380 @@ -83,7 +83,8 @@ public static class AspireOracleEFCoreDatabaseExtensions { meterProviderBuilder.AddEventCountersInstrumentation(eventCountersInstrumentationOptions => { - eventCountersInstrumentationOptions.AddEventSources("Oracle.EntityFrameworkCore.Database"); + // https://github.com/dotnet/efcore/blob/a1cd4f45aa18314bc91d2b9ea1f71a3b7d5bf636/src/EFCore/Infrastructure/EntityFrameworkEventSource.cs#L45 + eventCountersInstrumentationOptions.AddEventSources("Microsoft.EntityFrameworkCore"); }); }); } diff --git a/src/Components/Aspire.Oracle.EntityFrameworkCore/ConfigurationSchema.json b/src/Components/Aspire.Oracle.EntityFrameworkCore/ConfigurationSchema.json new file mode 100644 index 00000000000..e45a494236c --- /dev/null +++ b/src/Components/Aspire.Oracle.EntityFrameworkCore/ConfigurationSchema.json @@ -0,0 +1,95 @@ +{ + "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": { + "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" +} diff --git a/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/OracleEntityFrameworkCoreDatabaseSettings.cs b/src/Components/Aspire.Oracle.EntityFrameworkCore/OracleEntityFrameworkCoreSettings.cs similarity index 94% rename from src/Components/Aspire.Oracle.EntityFrameworkCore.Database/OracleEntityFrameworkCoreDatabaseSettings.cs rename to src/Components/Aspire.Oracle.EntityFrameworkCore/OracleEntityFrameworkCoreSettings.cs index 3e1938c8fd2..c8200fd6d0c 100644 --- a/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/OracleEntityFrameworkCoreDatabaseSettings.cs +++ b/src/Components/Aspire.Oracle.EntityFrameworkCore/OracleEntityFrameworkCoreSettings.cs @@ -1,12 +1,12 @@ // 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; +namespace Aspire.Oracle.EntityFrameworkCore; /// /// Provides the client configuration settings for connecting to a Oracle database using EntityFrameworkCore. /// -public sealed class OracleEntityFrameworkCoreDatabaseSettings +public sealed class OracleEntityFrameworkCoreSettings { /// /// The connection string of the Oracle database to connect to. diff --git a/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/README.md b/src/Components/Aspire.Oracle.EntityFrameworkCore/README.md similarity index 90% rename from src/Components/Aspire.Oracle.EntityFrameworkCore.Database/README.md rename to src/Components/Aspire.Oracle.EntityFrameworkCore/README.md index ea8ba9ddfc4..f0f9cbfa2d5 100644 --- a/src/Components/Aspire.Oracle.EntityFrameworkCore.Database/README.md +++ b/src/Components/Aspire.Oracle.EntityFrameworkCore/README.md @@ -88,6 +88,17 @@ Also you can pass the `Action configu builder.AddOracleDatabaseDbContext("orcl", settings => settings.HealthChecks = false); ``` +## AppHost extensions + + In your AppHost project, register an Oracle container and consume the connection using the following methods: + + ```csharp + var oracledb = builder.AddPostgresContainer("oracle").AddDatabase("freepdb1"); + + var myService = builder.AddProject() + .WithReference(oracledb); + ``` + ## Additional documentation * https://learn.microsoft.com/ef/core/ diff --git a/src/Components/Aspire_Components_Progress.md b/src/Components/Aspire_Components_Progress.md index 78d8ee14277..beec376e8f9 100644 --- a/src/Components/Aspire_Components_Progress.md +++ b/src/Components/Aspire_Components_Progress.md @@ -22,7 +22,7 @@ As part of the .NET Aspire November preview, we want to include a set of .NET As | StackExchange.Redis.OutputCaching | ✅ | ✅ | N/A | ✅ | ✅ | ✅ | ❌ | ✅ | | RabbitMQ | ✅ | ✅ | ✅ | ✅ | | | ❌ | ✅ | | MySqlConnector | ✅ | | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Oracle.EntityFrameworkCore.Database | ✅ | | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Oracle.EntityFrameworkCore | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | Nomenclature used in the table above: diff --git a/src/Components/Telemetry.md b/src/Components/Telemetry.md index f5b76e3e90d..ddfac8c932d 100644 --- a/src/Components/Telemetry.md +++ b/src/Components/Telemetry.md @@ -248,3 +248,30 @@ Aspire.StackExchange.Redis.OutputCaching: - Everything from `Aspire.StackExchange.Redis` plus: - Log categories: - "Microsoft.AspNetCore.OutputCaching.StackExchangeRedis" + +Aspire.Oracle.EntityFrameworkCore: +- Log categories: + - "Microsoft.EntityFrameworkCore.ChangeTracking" + - "Microsoft.EntityFrameworkCore.Database.Command" + - "Microsoft.EntityFrameworkCore.Database.Connection" + - "Microsoft.EntityFrameworkCore.Database.Transaction" + - "Microsoft.EntityFrameworkCore.Infrastructure" + - "Microsoft.EntityFrameworkCore.Migrations" + - "Microsoft.EntityFrameworkCore.Model" + - "Microsoft.EntityFrameworkCore.Model.Validation" + - "Microsoft.EntityFrameworkCore.Query" + - "Microsoft.EntityFrameworkCore.Update" +- Activity source names: + - "OpenTelemetry.Instrumentation.EntityFrameworkCore" +- Metric names: + - "Microsoft.EntityFrameworkCore": + - "ec_Microsoft_EntityFrameworkCore_active_db_contexts" + - "ec_Microsoft_EntityFrameworkCore_total_queries" + - "ec_Microsoft_EntityFrameworkCore_queries_per_second" + - "ec_Microsoft_EntityFrameworkCore_total_save_changes" + - "ec_Microsoft_EntityFrameworkCore_save_changes_per_second" + - "ec_Microsoft_EntityFrameworkCore_compiled_query_cache_hit_rate" + - "ec_Microsoft_Entity_total_execution_strategy_operation_failures" + - "ec_Microsoft_E_execution_strategy_operation_failures_per_second" + - "ec_Microsoft_EntityFramew_total_optimistic_concurrency_failures" + - "ec_Microsoft_EntityF_optimistic_concurrency_failures_per_second" diff --git a/tests/Aspire.Hosting.Tests/AddOracleDatabaseTests.cs b/tests/Aspire.Hosting.Tests/AddOracleDatabaseTests.cs new file mode 100644 index 00000000000..5eecfad4e34 --- /dev/null +++ b/tests/Aspire.Hosting.Tests/AddOracleDatabaseTests.cs @@ -0,0 +1,206 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Sockets; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Aspire.Hosting.Tests; + +public class AddOracleDatabaseTests +{ + [Fact] + public void AddOracleDatabaseWithDefaultsAddsAnnotationMetadata() + { + var appBuilder = DistributedApplication.CreateBuilder(); + appBuilder.AddOracleDatabaseContainer("orcl"); + + var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var containerResource = Assert.Single(appModel.GetContainerResources()); + Assert.Equal("orcl", containerResource.Name); + + var manifestPublishing = Assert.Single(containerResource.Annotations.OfType()); + Assert.NotNull(manifestPublishing.Callback); + + var containerAnnotation = Assert.Single(containerResource.Annotations.OfType()); + Assert.Equal("latest", containerAnnotation.Tag); + Assert.Equal("database/free", containerAnnotation.Image); + Assert.Equal("container-registry.oracle.com", containerAnnotation.Registry); + + var serviceBinding = Assert.Single(containerResource.Annotations.OfType()); + Assert.Equal(1521, serviceBinding.ContainerPort); + Assert.False(serviceBinding.IsExternal); + Assert.Equal("tcp", serviceBinding.Name); + Assert.Null(serviceBinding.Port); + Assert.Equal(ProtocolType.Tcp, serviceBinding.Protocol); + Assert.Equal("tcp", serviceBinding.Transport); + Assert.Equal("tcp", serviceBinding.UriScheme); + + var envAnnotations = containerResource.Annotations.OfType(); + + var config = new Dictionary(); + var context = new EnvironmentCallbackContext("dcp", config); + + foreach (var annotation in envAnnotations) + { + annotation.Callback(context); + } + + Assert.Collection(config, + env => + { + Assert.Equal("ORACLE_PWD", env.Key); + Assert.False(string.IsNullOrEmpty(env.Value)); + }); + } + + [Fact] + public void AddOracleDatabaseAddsAnnotationMetadata() + { + var appBuilder = DistributedApplication.CreateBuilder(); + appBuilder.AddOracleDatabaseContainer("orcl", 1234, "pass"); + + var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var containerResource = Assert.Single(appModel.GetContainerResources()); + Assert.Equal("orcl", containerResource.Name); + + var manifestPublishing = Assert.Single(containerResource.Annotations.OfType()); + Assert.NotNull(manifestPublishing.Callback); + + var containerAnnotation = Assert.Single(containerResource.Annotations.OfType()); + Assert.Equal("latest", containerAnnotation.Tag); + Assert.Equal("database/free", containerAnnotation.Image); + Assert.Equal("container-registry.oracle.com", containerAnnotation.Registry); + + var serviceBinding = Assert.Single(containerResource.Annotations.OfType()); + Assert.Equal(1521, serviceBinding.ContainerPort); + Assert.False(serviceBinding.IsExternal); + Assert.Equal("tcp", serviceBinding.Name); + Assert.Equal(1234, serviceBinding.Port); + Assert.Equal(ProtocolType.Tcp, serviceBinding.Protocol); + Assert.Equal("tcp", serviceBinding.Transport); + Assert.Equal("tcp", serviceBinding.UriScheme); + + var envAnnotations = containerResource.Annotations.OfType(); + + var config = new Dictionary(); + var context = new EnvironmentCallbackContext("dcp", config); + + foreach (var annotation in envAnnotations) + { + annotation.Callback(context); + } + + Assert.Collection(config, + env => + { + Assert.Equal("ORACLE_PWD", env.Key); + Assert.Equal("pass", env.Value); + }); + } + + [Fact] + public void OracleCreatesConnectionString() + { + var appBuilder = DistributedApplication.CreateBuilder(); + appBuilder.AddOracleDatabaseContainer("orcl") + .WithAnnotation( + new AllocatedEndpointAnnotation("mybinding", + ProtocolType.Tcp, + "localhost", + 2000, + "https" + )); + + var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var connectionStringResource = Assert.Single(appModel.Resources.OfType()); + var connectionString = connectionStringResource.GetConnectionString(); + + Assert.StartsWith("user id=system;password=", connectionString); + Assert.EndsWith(";data source=localhost:2000", connectionString); + } + + [Fact] + public void OracleCreatesConnectionStringWithDatabase() + { + var appBuilder = DistributedApplication.CreateBuilder(); + appBuilder.AddOracleDatabaseContainer("orcl") + .WithAnnotation( + new AllocatedEndpointAnnotation("mybinding", + ProtocolType.Tcp, + "localhost", + 2000, + "https" + )) + .AddDatabase("db"); + + var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + + var oracleResource = Assert.Single(appModel.Resources.OfType()); + var oracleConnectionString = oracleResource.GetConnectionString(); + var oracleDatabaseResource = Assert.Single(appModel.Resources.OfType()); + var dbConnectionString = oracleDatabaseResource.GetConnectionString(); + + Assert.Equal(oracleConnectionString + "/db", dbConnectionString); + } + + [Fact] + public void AddDatabaseToOracleDatabaseAddsAnnotationMetadata() + { + var appBuilder = DistributedApplication.CreateBuilder(); + appBuilder.AddOracleDatabaseContainer("oracle", 1234, "pass").AddDatabase("db"); + + var app = appBuilder.Build(); + + var appModel = app.Services.GetRequiredService(); + var containerResources = appModel.GetContainerResources(); + + var containerResource = Assert.Single(containerResources); + Assert.Equal("oracle", containerResource.Name); + + var manifestPublishing = Assert.Single(containerResource.Annotations.OfType()); + Assert.NotNull(manifestPublishing.Callback); + + var containerAnnotation = Assert.Single(containerResource.Annotations.OfType()); + Assert.Equal("latest", containerAnnotation.Tag); + Assert.Equal("database/free", containerAnnotation.Image); + Assert.Equal("container-registry.oracle.com", containerAnnotation.Registry); + + var serviceBinding = Assert.Single(containerResource.Annotations.OfType()); + Assert.Equal(1521, serviceBinding.ContainerPort); + Assert.False(serviceBinding.IsExternal); + Assert.Equal("tcp", serviceBinding.Name); + Assert.Equal(1234, serviceBinding.Port); + Assert.Equal(ProtocolType.Tcp, serviceBinding.Protocol); + Assert.Equal("tcp", serviceBinding.Transport); + Assert.Equal("tcp", serviceBinding.UriScheme); + + var envAnnotations = containerResource.Annotations.OfType(); + + var config = new Dictionary(); + var context = new EnvironmentCallbackContext("dcp", config); + + foreach (var annotation in envAnnotations) + { + annotation.Callback(context); + } + + Assert.Collection(config, + env => + { + Assert.Equal("ORACLE_PWD", env.Key); + Assert.Equal("pass", env.Value); + }); + } +} diff --git a/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests.csproj b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/Aspire.Oracle.EntityFrameworkCore.Tests.csproj similarity index 87% rename from tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests.csproj rename to tests/Aspire.Oracle.EntityFrameworkCore.Tests/Aspire.Oracle.EntityFrameworkCore.Tests.csproj index 4568b53998f..26e5a5b30ed 100644 --- a/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests.csproj +++ b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/Aspire.Oracle.EntityFrameworkCore.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/AspireOracleEFCoreDatabaseExtensionsTests.cs b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/AspireOracleEFCoreDatabaseExtensionsTests.cs similarity index 95% rename from tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/AspireOracleEFCoreDatabaseExtensionsTests.cs rename to tests/Aspire.Oracle.EntityFrameworkCore.Tests/AspireOracleEFCoreDatabaseExtensionsTests.cs index 0a4ad3bcd42..c7756e07a66 100644 --- a/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/AspireOracleEFCoreDatabaseExtensionsTests.cs +++ b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/AspireOracleEFCoreDatabaseExtensionsTests.cs @@ -12,7 +12,7 @@ using Oracle.EntityFrameworkCore.Infrastructure.Internal; using Xunit; -namespace Aspire.Oracle.EntityFrameworkCore.Database.Tests; +namespace Aspire.Oracle.EntityFrameworkCore.Tests; public class AspireOracleEFCoreDatabaseExtensionsTests { @@ -58,7 +58,7 @@ public void ConnectionNameWinsOverConfigSection() { var builder = Host.CreateEmptyApplicationBuilder(null); builder.Configuration.AddInMemoryCollection([ - new KeyValuePair("Aspire:Oracle:EntityFrameworkCore:Database:ConnectionString", "unused"), + new KeyValuePair("Aspire:Oracle:EntityFrameworkCore:ConnectionString", "unused"), new KeyValuePair("ConnectionStrings:orclconnection", ConnectionString) ]); @@ -79,8 +79,8 @@ public void CanConfigureDbContextOptions() var builder = Host.CreateEmptyApplicationBuilder(null); builder.Configuration.AddInMemoryCollection([ new KeyValuePair("ConnectionStrings:orclconnection", ConnectionString), - new KeyValuePair("Aspire:Oracle:EntityFrameworkCore:Database:MaxRetryCount", "304"), - new KeyValuePair("Aspire:Oracle:EntityFrameworkCore:Database:Timeout", "608") + new KeyValuePair("Aspire:Oracle:EntityFrameworkCore:MaxRetryCount", "304"), + new KeyValuePair("Aspire:Oracle:EntityFrameworkCore:Timeout", "608") ]); builder.AddOracleDatabaseDbContext("orclconnection", configureDbContextOptions: optionsBuilder => diff --git a/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_NoPooling.cs b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/ConformanceTests_NoPooling.cs similarity index 80% rename from tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_NoPooling.cs rename to tests/Aspire.Oracle.EntityFrameworkCore.Tests/ConformanceTests_NoPooling.cs index c4b73c4086b..a26f7e14d98 100644 --- a/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_NoPooling.cs +++ b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/ConformanceTests_NoPooling.cs @@ -5,13 +5,13 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace Aspire.Oracle.EntityFrameworkCore.Database.Tests; +namespace Aspire.Oracle.EntityFrameworkCore.Tests; public class ConformanceTests_NoPooling : ConformanceTests_Pooling { protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Scoped; - protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null) + protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null) { builder.AddOracleDatabaseDbContext("orclconnection", settings => { diff --git a/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_NoPooling_TypeSpecificConfig.cs b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/ConformanceTests_NoPooling_TypeSpecificConfig.cs similarity index 73% rename from tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_NoPooling_TypeSpecificConfig.cs rename to tests/Aspire.Oracle.EntityFrameworkCore.Tests/ConformanceTests_NoPooling_TypeSpecificConfig.cs index 94215fd3720..f4191ebc303 100644 --- a/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_NoPooling_TypeSpecificConfig.cs +++ b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/ConformanceTests_NoPooling_TypeSpecificConfig.cs @@ -4,13 +4,13 @@ using Aspire.Components.Common.Tests; using Microsoft.Extensions.Configuration; -namespace Aspire.Oracle.EntityFrameworkCore.Database.Tests; +namespace Aspire.Oracle.EntityFrameworkCore.Tests; public class ConformanceTests_NoPooling_TypeSpecificConfig : ConformanceTests_NoPooling { protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null) => configuration.AddInMemoryCollection(new KeyValuePair[1] { - new($"Aspire:Oracle:EntityFrameworkCore:Database:{typeof(TestDbContext).Name}:ConnectionString", ConnectionString) + new($"Aspire:Oracle:EntityFrameworkCore:{typeof(TestDbContext).Name}:ConnectionString", ConnectionString) }); } diff --git a/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_Pooling.cs b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/ConformanceTests_Pooling.cs similarity index 80% rename from tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_Pooling.cs rename to tests/Aspire.Oracle.EntityFrameworkCore.Tests/ConformanceTests_Pooling.cs index 827ed595509..9c5e48ae77e 100644 --- a/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_Pooling.cs +++ b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/ConformanceTests_Pooling.cs @@ -10,9 +10,9 @@ using Microsoft.Extensions.Hosting; using Xunit; -namespace Aspire.Oracle.EntityFrameworkCore.Database.Tests; +namespace Aspire.Oracle.EntityFrameworkCore.Tests; -public class ConformanceTests_Pooling : ConformanceTests +public class ConformanceTests_Pooling : ConformanceTests { protected const string ConnectionString = "Data Source=fake;"; @@ -36,20 +36,18 @@ public class ConformanceTests_Pooling : ConformanceTests "src/Components/Aspire.Oracle.EntityFrameworkCore.Database/ConfigurationSchema.json"; + protected override string JsonSchemaPath => "src/Components/Aspire.Oracle.EntityFrameworkCore/ConfigurationSchema.json"; protected override string ValidJsonConfig => """ { "Aspire": { "Oracle": { "EntityFrameworkCore": { - "Database": { - "ConnectionString": "YOUR_CONNECTION_STRING", - "HealthChecks": false, - "DbContextPooling": true, - "Tracing": true, - "Metrics": true - } + "ConnectionString": "YOUR_CONNECTION_STRING", + "HealthChecks": false, + "DbContextPooling": true, + "Tracing": true, + "Metrics": true } } } @@ -58,27 +56,27 @@ public class ConformanceTests_Pooling : ConformanceTests new[] { - ("""{"Aspire": { "Oracle": { "EntityFrameworkCore":{ "Database": { "MaxRetryCount": "5"}}}}}""", "Value is \"string\" but should be \"integer\""), - ("""{"Aspire": { "Oracle": { "EntityFrameworkCore":{ "Database": { "HealthChecks": "false"}}}}}""", "Value is \"string\" but should be \"boolean\""), - ("""{"Aspire": { "Oracle": { "EntityFrameworkCore":{ "Database": { "ConnectionString": "", "DbContextPooling": "Yes"}}}}}""", "Value is \"string\" but should be \"boolean\"") + ("""{"Aspire": { "Oracle": { "EntityFrameworkCore":{ "MaxRetryCount": "5"}}}}""", "Value is \"string\" but should be \"integer\""), + ("""{"Aspire": { "Oracle": { "EntityFrameworkCore":{ "HealthChecks": "false"}}}}""", "Value is \"string\" but should be \"boolean\""), + ("""{"Aspire": { "Oracle": { "EntityFrameworkCore":{ "ConnectionString": "", "DbContextPooling": "Yes"}}}}""", "Value is \"string\" but should be \"boolean\"") }; protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null) => configuration.AddInMemoryCollection(new KeyValuePair[1] { - new KeyValuePair("Aspire:Oracle:EntityFrameworkCore:Database:ConnectionString", ConnectionString) + new KeyValuePair("Aspire:Oracle:EntityFrameworkCore:ConnectionString", ConnectionString) }); - protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null) + protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null) => builder.AddOracleDatabaseDbContext("orclconnection", configure); - protected override void SetHealthCheck(OracleEntityFrameworkCoreDatabaseSettings options, bool enabled) + protected override void SetHealthCheck(OracleEntityFrameworkCoreSettings options, bool enabled) => options.HealthChecks = enabled; - protected override void SetTracing(OracleEntityFrameworkCoreDatabaseSettings options, bool enabled) + protected override void SetTracing(OracleEntityFrameworkCoreSettings options, bool enabled) => options.Tracing = enabled; - protected override void SetMetrics(OracleEntityFrameworkCoreDatabaseSettings options, bool enabled) + protected override void SetMetrics(OracleEntityFrameworkCoreSettings options, bool enabled) => options.Metrics = enabled; protected override void TriggerActivity(TestDbContext service) diff --git a/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_Pooling_TypeSpecificConfig.cs b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/ConformanceTests_Pooling_TypeSpecificConfig.cs similarity index 73% rename from tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_Pooling_TypeSpecificConfig.cs rename to tests/Aspire.Oracle.EntityFrameworkCore.Tests/ConformanceTests_Pooling_TypeSpecificConfig.cs index f3fb8ccd987..c75e7d7d9a6 100644 --- a/tests/Aspire.Oracle.EntityFrameworkCore.Database.Tests/ConformanceTests_Pooling_TypeSpecificConfig.cs +++ b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/ConformanceTests_Pooling_TypeSpecificConfig.cs @@ -4,13 +4,13 @@ using Aspire.Components.Common.Tests; using Microsoft.Extensions.Configuration; -namespace Aspire.Oracle.EntityFrameworkCore.Database.Tests; +namespace Aspire.Oracle.EntityFrameworkCore.Tests; public class ConformanceTests_Pooling_TypeSpecificConfig : ConformanceTests_Pooling { protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null) => configuration.AddInMemoryCollection(new KeyValuePair[1] { - new($"Aspire:Oracle:EntityFrameworkCore:Database:{typeof(TestDbContext).Name}:ConnectionString", ConnectionString) + new($"Aspire:Oracle:EntityFrameworkCore:{typeof(TestDbContext).Name}:ConnectionString", ConnectionString) }); } From 91e0b7b3cb7ad72d1a2c109bb049a9ec80b9c1cb Mon Sep 17 00:00:00 2001 From: Andre Lins Date: Fri, 15 Dec 2023 14:46:21 -0300 Subject: [PATCH 07/12] Oracle has a 30 character password limit --- src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs index 932575792b0..f8191b21885 100644 --- a/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs +++ b/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs @@ -24,7 +24,7 @@ public static class OracleDatabaseBuilderExtensions /// A reference to the . public static IResourceBuilder AddOracleDatabaseContainer(this IDistributedApplicationBuilder builder, string name, int? port = null, string? password = null) { - password = password ?? Guid.NewGuid().ToString("N"); + password = password ?? Guid.NewGuid().ToString("N").Substring(0, 30); var oracleDatabaseContainer = new OracleDatabaseContainerResource(name, password); return builder.AddResource(oracleDatabaseContainer) .WithManifestPublishingCallback(context => WriteOracleDatabaseContainerResourceToManifest(context, oracleDatabaseContainer)) @@ -51,7 +51,7 @@ public static IResourceBuilder AddOracleDatabas /// A reference to the . public static IResourceBuilder AddOracleDatabase(this IDistributedApplicationBuilder builder, string name) { - var password = Guid.NewGuid().ToString("N"); + var password = Guid.NewGuid().ToString("N").Substring(0, 30); var oracleDatabaseServer = new OracleDatabaseServerResource(name, password); return builder.AddResource(oracleDatabaseServer) .WithManifestPublishingCallback(WriteOracleDatabaseContainerToManifest) From 74a10a0de95d06cdb9282ec3358a847a4b02c075 Mon Sep 17 00:00:00 2001 From: Andre Lins Date: Fri, 15 Dec 2023 14:51:24 -0300 Subject: [PATCH 08/12] functional test --- .../{ => Oracle}/AddOracleDatabaseTests.cs | 2 +- .../Oracle/OracleDatabaseFunctionalTests.cs | 31 +++++++++++++++++++ .../TestProject.AppHost/TestProgram.cs | 8 ++++- .../Oracle/OracleDatabaseExtensions.cs | 30 ++++++++++++++++++ .../OracleManagedDataAccessExtensions.cs | 24 ++++++++++++++ .../Program.cs | 4 +++ .../TestProject.IntegrationServiceA.csproj | 2 +- 7 files changed, 98 insertions(+), 3 deletions(-) rename tests/Aspire.Hosting.Tests/{ => Oracle}/AddOracleDatabaseTests.cs (99%) create mode 100644 tests/Aspire.Hosting.Tests/Oracle/OracleDatabaseFunctionalTests.cs create mode 100644 tests/testproject/TestProject.IntegrationServiceA/Oracle/OracleDatabaseExtensions.cs create mode 100644 tests/testproject/TestProject.IntegrationServiceA/Oracle/OracleManagedDataAccessExtensions.cs diff --git a/tests/Aspire.Hosting.Tests/AddOracleDatabaseTests.cs b/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs similarity index 99% rename from tests/Aspire.Hosting.Tests/AddOracleDatabaseTests.cs rename to tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs index 5eecfad4e34..035d1613f60 100644 --- a/tests/Aspire.Hosting.Tests/AddOracleDatabaseTests.cs +++ b/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Aspire.Hosting.Tests; +namespace Aspire.Hosting.Tests.Oracle; public class AddOracleDatabaseTests { diff --git a/tests/Aspire.Hosting.Tests/Oracle/OracleDatabaseFunctionalTests.cs b/tests/Aspire.Hosting.Tests/Oracle/OracleDatabaseFunctionalTests.cs new file mode 100644 index 00000000000..c2c026cb9e9 --- /dev/null +++ b/tests/Aspire.Hosting.Tests/Oracle/OracleDatabaseFunctionalTests.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.Tests.Helpers; +using Xunit; + +namespace Aspire.Hosting.Tests.Oracle; + +[Collection("IntegrationServices")] +public class OracleDatabaseFunctionalTests +{ + private readonly IntegrationServicesFixture _integrationServicesFixture; + + public OracleDatabaseFunctionalTests(IntegrationServicesFixture integrationServicesFixture) + { + _integrationServicesFixture = integrationServicesFixture; + } + + [LocalOnlyFact()] + public async Task VerifyOracleDatabaseWorks() + { + var testProgram = _integrationServicesFixture.TestProgram; + var client = _integrationServicesFixture.HttpClient; + + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/oracledatabase/verify", cts.Token); + + Assert.True(response.IsSuccessStatusCode); + } +} diff --git a/tests/testproject/TestProject.AppHost/TestProgram.cs b/tests/testproject/TestProject.AppHost/TestProgram.cs index b5f1a20e71b..2a6ffe20a94 100644 --- a/tests/testproject/TestProject.AppHost/TestProgram.cs +++ b/tests/testproject/TestProject.AppHost/TestProgram.cs @@ -36,6 +36,7 @@ private TestProgram(string[] args, Assembly assembly, bool includeIntegrationSer var mysqlDbName = "mysqldb"; var postgresDbName = "postgresdb"; var mongoDbName = "mymongodb"; + var oracleDbName = "freepdb1"; var sqlserverContainer = AppBuilder.AddSqlServerContainer("sqlservercontainer") .AddDatabase(sqlserverDbName); @@ -49,6 +50,8 @@ private TestProgram(string[] args, Assembly assembly, bool includeIntegrationSer var rabbitmqContainer = AppBuilder.AddRabbitMQContainer("rabbitmqcontainer"); var mongodbContainer = AppBuilder.AddMongoDBContainer("mongodbcontainer") .AddDatabase(mongoDbName); + var oracleDatabaseContainer = AppBuilder.AddOracleDatabaseContainer("oracledatabasecontainer") + .AddDatabase(oracleDbName); var sqlserverAbstract = AppBuilder.AddSqlServerContainer("sqlserverabstract"); var mysqlAbstract = AppBuilder.AddMySqlContainer("mysqlabstract"); @@ -56,6 +59,7 @@ private TestProgram(string[] args, Assembly assembly, bool includeIntegrationSer var postgresAbstract = AppBuilder.AddPostgresContainer("postgresabstract"); var rabbitmqAbstract = AppBuilder.AddRabbitMQContainer("rabbitmqabstract"); var mongodbAbstract = AppBuilder.AddMongoDB("mongodbabstract"); + var oracleDatabaseAbstract = AppBuilder.AddOracleDatabaseContainer("oracledatabaseabstract"); IntegrationServiceABuilder = AppBuilder.AddProject("integrationservicea") .WithReference(sqlserverContainer) @@ -64,12 +68,14 @@ private TestProgram(string[] args, Assembly assembly, bool includeIntegrationSer .WithReference(postgresContainer) .WithReference(rabbitmqContainer) .WithReference(mongodbContainer) + .WithReference(oracleDatabaseContainer) .WithReference(sqlserverAbstract) .WithReference(mysqlAbstract) .WithReference(redisAbstract) .WithReference(postgresAbstract) .WithReference(rabbitmqAbstract) - .WithReference(mongodbAbstract); + .WithReference(mongodbAbstract) + .WithReference(oracleDatabaseAbstract); } } diff --git a/tests/testproject/TestProject.IntegrationServiceA/Oracle/OracleDatabaseExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/Oracle/OracleDatabaseExtensions.cs new file mode 100644 index 00000000000..208127eb1cd --- /dev/null +++ b/tests/testproject/TestProject.IntegrationServiceA/Oracle/OracleDatabaseExtensions.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Oracle.ManagedDataAccess.Client; + +public static class OracleDatabaseExtensions +{ + public static void MapOracleDatabaseApi(this WebApplication app) + { + app.MapGet("/oracledatabase/verify", VerifyOracleDatabaseAsync); + } + + private static async Task VerifyOracleDatabaseAsync(OracleConnection connection) + { + try + { + await connection.OpenAsync(); + + var command = connection.CreateCommand(); + command.CommandText = $"SELECT 1 FROM DUAL"; + var results = await command.ExecuteReaderAsync(); + + return results.HasRows ? Results.Ok("Success!") : Results.Problem("Failed"); + } + catch (Exception e) + { + return Results.Problem(e.ToString()); + } + } +} diff --git a/tests/testproject/TestProject.IntegrationServiceA/Oracle/OracleManagedDataAccessExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/Oracle/OracleManagedDataAccessExtensions.cs new file mode 100644 index 00000000000..98e710e379b --- /dev/null +++ b/tests/testproject/TestProject.IntegrationServiceA/Oracle/OracleManagedDataAccessExtensions.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Oracle.ManagedDataAccess.Client; + +// This is a workaround while https://github.com/dotnet/aspire/pull/1004 is not merged. +public static class OracleManagedDataAccessExtensions +{ + public static void AddOracleClient(this WebApplicationBuilder builder, string connectionName) + { + ArgumentNullException.ThrowIfNull(builder); + + var connectionString = builder.Configuration.GetConnectionString(connectionName); + builder.Services.AddScoped(_ => new OracleConnection(connectionString)); + } + + public static void AddKeyedOracleClient(this WebApplicationBuilder builder, string connectionName) + { + ArgumentNullException.ThrowIfNull(builder); + + var connectionString = builder.Configuration.GetConnectionString(connectionName); + builder.Services.AddKeyedSingleton(connectionName, (serviceProvider, _) => new OracleConnection(connectionString)); + } +} diff --git a/tests/testproject/TestProject.IntegrationServiceA/Program.cs b/tests/testproject/TestProject.IntegrationServiceA/Program.cs index b809f3e7d84..a4d7784dcf6 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/Program.cs +++ b/tests/testproject/TestProject.IntegrationServiceA/Program.cs @@ -8,6 +8,7 @@ builder.AddNpgsqlDataSource("postgresdb"); builder.AddRabbitMQ("rabbitmqcontainer"); builder.AddMongoDBClient("mymongodb"); +builder.AddOracleClient("freepdb1"); builder.AddKeyedSqlServerClient("sqlserverabstract"); builder.AddKeyedMySqlDataSource("mysqlabstract"); @@ -15,6 +16,7 @@ builder.AddKeyedNpgsqlDataSource("postgresabstract"); builder.AddKeyedRabbitMQ("rabbitmqabstract"); builder.AddKeyedMongoDBClient("mongodbabstract"); +builder.AddKeyedOracleClient("oracledatabaseabstract"); var app = builder.Build(); @@ -36,4 +38,6 @@ app.MapRabbitMQApi(); +app.MapOracleDatabaseApi(); + app.Run(); diff --git a/tests/testproject/TestProject.IntegrationServiceA/TestProject.IntegrationServiceA.csproj b/tests/testproject/TestProject.IntegrationServiceA/TestProject.IntegrationServiceA.csproj index 95446906908..4b8406eeada 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/TestProject.IntegrationServiceA.csproj +++ b/tests/testproject/TestProject.IntegrationServiceA/TestProject.IntegrationServiceA.csproj @@ -14,7 +14,7 @@ - + From e04cee196507ba180a0de134cfb1f06ebaed02f1 Mon Sep 17 00:00:00 2001 From: Andre Lins Date: Fri, 15 Dec 2023 22:14:34 -0300 Subject: [PATCH 09/12] adjusting to reviews --- .../Aspire.Oracle.EntityFrameworkCore.csproj | 1 + .../AssemblyInfo.cs | 21 ++++++++ .../ConfigurationSchema.json | 36 ++++++------- .../OracleEntityFrameworkCoreSettings.cs | 18 ++----- .../README.md | 32 +++++++----- src/Components/Telemetry.md | 52 +++++++++---------- .../ConformanceTests_Pooling.cs | 3 +- .../Oracle/MyDbContext.cs | 11 ++++ .../Oracle/OracleDatabaseExtensions.cs | 15 ++---- .../OracleManagedDataAccessExtensions.cs | 24 --------- .../Program.cs | 3 +- 11 files changed, 106 insertions(+), 110 deletions(-) create mode 100644 src/Components/Aspire.Oracle.EntityFrameworkCore/AssemblyInfo.cs create mode 100644 tests/testproject/TestProject.IntegrationServiceA/Oracle/MyDbContext.cs delete mode 100644 tests/testproject/TestProject.IntegrationServiceA/Oracle/OracleManagedDataAccessExtensions.cs diff --git a/src/Components/Aspire.Oracle.EntityFrameworkCore/Aspire.Oracle.EntityFrameworkCore.csproj b/src/Components/Aspire.Oracle.EntityFrameworkCore/Aspire.Oracle.EntityFrameworkCore.csproj index 7f9465d64c9..37943f18e42 100644 --- a/src/Components/Aspire.Oracle.EntityFrameworkCore/Aspire.Oracle.EntityFrameworkCore.csproj +++ b/src/Components/Aspire.Oracle.EntityFrameworkCore/Aspire.Oracle.EntityFrameworkCore.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Components/Aspire.Oracle.EntityFrameworkCore/AssemblyInfo.cs b/src/Components/Aspire.Oracle.EntityFrameworkCore/AssemblyInfo.cs new file mode 100644 index 00000000000..80b98ac96ba --- /dev/null +++ b/src/Components/Aspire.Oracle.EntityFrameworkCore/AssemblyInfo.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Oracle.EntityFrameworkCore; +using Aspire; + +[assembly: ConfigurationSchema("Aspire:Oracle:EntityFrameworkCore", typeof(OracleEntityFrameworkCoreSettings))] + +[assembly: LoggingCategories( + "Microsoft.EntityFrameworkCore", + "Microsoft.EntityFrameworkCore.ChangeTracking", + "Microsoft.EntityFrameworkCore.Database", + "Microsoft.EntityFrameworkCore.Database.Command", + "Microsoft.EntityFrameworkCore.Database.Transaction", + "Microsoft.EntityFrameworkCore.Database.Connection", + "Microsoft.EntityFrameworkCore.Infrastructure", + "Microsoft.EntityFrameworkCore.Migrations", + "Microsoft.EntityFrameworkCore.Model", + "Microsoft.EntityFrameworkCore.Model.Validation", + "Microsoft.EntityFrameworkCore.Query", + "Microsoft.EntityFrameworkCore.Update")] diff --git a/src/Components/Aspire.Oracle.EntityFrameworkCore/ConfigurationSchema.json b/src/Components/Aspire.Oracle.EntityFrameworkCore/ConfigurationSchema.json index e45a494236c..56695f80e01 100644 --- a/src/Components/Aspire.Oracle.EntityFrameworkCore/ConfigurationSchema.json +++ b/src/Components/Aspire.Oracle.EntityFrameworkCore/ConfigurationSchema.json @@ -14,10 +14,10 @@ "Microsoft.EntityFrameworkCore.Database.Command": { "$ref": "#/definitions/logLevelThreshold" }, - "Microsoft.EntityFrameworkCore.Database.Transaction": { + "Microsoft.EntityFrameworkCore.Database.Connection": { "$ref": "#/definitions/logLevelThreshold" }, - "Microsoft.EntityFrameworkCore.Database.Connection": { + "Microsoft.EntityFrameworkCore.Database.Transaction": { "$ref": "#/definitions/logLevelThreshold" }, "Microsoft.EntityFrameworkCore.Infrastructure": { @@ -53,38 +53,34 @@ "properties": { "ConnectionString": { "type": "string", - "description": "Gets or sets the connection string of the SQL Server database to connect to." + "description": "The connection string of the Oracle 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 + "description": "Gets or sets a boolean value that indicates whether the db context will be pooled or explicitly created every time it\u0027s requested." }, "HealthChecks": { "type": "boolean", - "description": "Gets or sets a boolean value that indicates whether the DbContext health check is enabled or not.", - "default": true + "description": "Gets or sets a boolean value that indicates whether the database health check is enabled or not. The default value is true." }, - "Tracing": { - "type": "boolean", - "description": "Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is enabled or not.", - "default": true + "MaxRetryCount": { + "type": "integer", + "description": "Gets or sets the maximum number of retry attempts. The default is 6. Set it to 0 to disable the retry mechanism." }, "Metrics": { "type": "boolean", - "description": "Gets or sets a boolean value that indicates whether the OpenTelemetry metrics are enabled or not.", - "default": true + "description": "Gets or sets a boolean value that indicates whether the Open Telemetry metrics are enabled or not. The default value is true." }, "Timeout": { "type": "integer", - "description": "Gets or sets the time in seconds to wait for the command to execute.", - "default": null + "description": "The time in seconds to wait for the command to execute." + }, + "Tracing": { + "type": "boolean", + "description": "Gets or sets a boolean value that indicates whether the Open Telemetry tracing is enabled or not. The default value is true." } - } + }, + "description": "Provides the client configuration settings for connecting to a Oracle database using EntityFrameworkCore." } } } diff --git a/src/Components/Aspire.Oracle.EntityFrameworkCore/OracleEntityFrameworkCoreSettings.cs b/src/Components/Aspire.Oracle.EntityFrameworkCore/OracleEntityFrameworkCoreSettings.cs index c8200fd6d0c..6c869a873e8 100644 --- a/src/Components/Aspire.Oracle.EntityFrameworkCore/OracleEntityFrameworkCoreSettings.cs +++ b/src/Components/Aspire.Oracle.EntityFrameworkCore/OracleEntityFrameworkCoreSettings.cs @@ -20,34 +20,26 @@ public sealed class OracleEntityFrameworkCoreSettings /// /// Gets or sets the maximum number of retry attempts. - /// - /// The default is 6. - /// Set it to 0 to disable the retry mechanism. - /// + /// The default is 6. + /// Set it to 0 to disable the retry mechanism. /// public int MaxRetryCount { get; set; } = 6; /// /// Gets or sets a boolean value that indicates whether the database health check is enabled or not. - /// - /// The default value is . - /// + /// The default value is . /// public bool HealthChecks { get; set; } = true; /// /// Gets or sets a boolean value that indicates whether the Open Telemetry tracing is enabled or not. - /// - /// The default value is . - /// + /// The default value is . /// public bool Tracing { get; set; } = true; /// /// Gets or sets a boolean value that indicates whether the Open Telemetry metrics are enabled or not. - /// - /// The default value is . - /// + /// The default value is . /// public bool Metrics { get; set; } = true; diff --git a/src/Components/Aspire.Oracle.EntityFrameworkCore/README.md b/src/Components/Aspire.Oracle.EntityFrameworkCore/README.md index f0f9cbfa2d5..3c46bcc28f4 100644 --- a/src/Components/Aspire.Oracle.EntityFrameworkCore/README.md +++ b/src/Components/Aspire.Oracle.EntityFrameworkCore/README.md @@ -1,4 +1,4 @@ -# Aspire.Oracle.EntityFrameworkCore.Database library +# Aspire.Oracle.EntityFrameworkCore library Registers [EntityFrameworkCore](https://learn.microsoft.com/ef/core/) [DbContext](https://learn.microsoft.com/dotnet/api/microsoft.entityframeworkcore.dbcontext) service for connecting Oracle database. Enables connection pooling, health check, logging and telemetry. @@ -10,10 +10,10 @@ Registers [EntityFrameworkCore](https://learn.microsoft.com/ef/core/) [DbContext ### Install the package -Install the .NET Aspire Oracle EntityFrameworkCore Database library with [NuGet](https://www.nuget.org): +Install the .NET Aspire Oracle EntityFrameworkCore library with [NuGet](https://www.nuget.org): ```dotnetcli -dotnet add package Aspire.Oracle.EntityFrameworkCore.Database +dotnet add package Aspire.Oracle.EntityFrameworkCore ``` ## Usage example @@ -37,7 +37,7 @@ public ProductsController(MyDbContext context) ## Configuration -The .NET Aspire Oracle EntityFrameworkCore Database component provides multiple options to configure the SQL connection based on the requirements and conventions of your project. +The .NET Aspire Oracle EntityFrameworkCore component provides multiple options to configure the database connection based on the requirements and conventions of your project. ### Use a connection string @@ -61,19 +61,17 @@ See the [ODP.NET documentation](https://docs.oracle.com/en/database/oracle/oracl ### Use configuration providers -The .NET Aspire Oracle EntityFrameworkCore Database component supports [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/api/microsoft.extensions.configuration). It loads the `OracleEntityFrameworkCoreDatabaseSettings` from configuration by using the `Aspire:Microsoft:EntityFrameworkCore:OracleDatabase` key. Example `appsettings.json` that configures some of the options: +The .NET Aspire Oracle EntityFrameworkCore component supports [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/api/microsoft.extensions.configuration). It loads the `OracleEntityFrameworkCoreSettings` from configuration by using the `Aspire:Oracle:EntityFrameworkCore` key. Example `appsettings.json` that configures some of the options: ```json { "Aspire": { "Oracle": { "EntityFrameworkCore": { - "Database": { - "DbContextPooling": true, - "HealthChecks": false, - "Tracing": false, - "Metrics": true - } + "DbContextPooling": true, + "HealthChecks": false, + "Tracing": false, + "Metrics": true } } } @@ -82,7 +80,7 @@ The .NET Aspire Oracle EntityFrameworkCore Database component supports [Microsof ### Use inline delegates -Also you can pass the `Action configureSettings` delegate to set up some or all the options inline, for example to disable health checks from code: +Also you can pass the `Action configureSettings` delegate to set up some or all the options inline, for example to disable health checks from code: ```csharp builder.AddOracleDatabaseDbContext("orcl", settings => settings.HealthChecks = false); @@ -93,12 +91,18 @@ Also you can pass the `Action configu In your AppHost project, register an Oracle container and consume the connection using the following methods: ```csharp - var oracledb = builder.AddPostgresContainer("oracle").AddDatabase("freepdb1"); + var freepdb1 = builder.AddOracleDatabaseContainer("oracle").AddDatabase("freepdb1"); var myService = builder.AddProject() - .WithReference(oracledb); + .WithReference(freepdb1); ``` +The `WithReference` method configures a connection in the `MyService` project named `freepdb1`. In the _Program.cs_ file of `MyService`, the database connection can be consumed using: + +```csharp +builder.AddOracleDatabaseDbContext("freepdb1"); +``` + ## Additional documentation * https://learn.microsoft.com/ef/core/ diff --git a/src/Components/Telemetry.md b/src/Components/Telemetry.md index ddfac8c932d..cb8df79853b 100644 --- a/src/Components/Telemetry.md +++ b/src/Components/Telemetry.md @@ -223,32 +223,6 @@ Aspire.Npgsql.EntityFrameworkCore.PostgreSQL: - "db.client.connections.timeouts" - "db.client.connections.usage" -Aspire.RabbitMQ.Client: -- Log categories: - - "RabbitMQ.Client" -- Activity source names: - - "Aspire.RabbitMQ.Client" -- Metric names: - - none (currently not supported by RabbitMQ.Client library) - -Aspire.StackExchange.Redis: -- Log categories: - - "Aspire.StackExchange.Redis" (this name is defined by our component, we can change it) -- Activity source names: - - "OpenTelemetry.Instrumentation.StackExchangeRedis" -- Metric names: - - none (currently not supported by StackExchange.Redis library) - -Aspire.StackExchange.Redis.DistributedCaching: -- Everything from `Aspire.StackExchange.Redis` plus: -- Log categories: - - "Microsoft.Extensions.Caching.StackExchangeRedis" - -Aspire.StackExchange.Redis.OutputCaching: -- Everything from `Aspire.StackExchange.Redis` plus: -- Log categories: - - "Microsoft.AspNetCore.OutputCaching.StackExchangeRedis" - Aspire.Oracle.EntityFrameworkCore: - Log categories: - "Microsoft.EntityFrameworkCore.ChangeTracking" @@ -275,3 +249,29 @@ Aspire.Oracle.EntityFrameworkCore: - "ec_Microsoft_E_execution_strategy_operation_failures_per_second" - "ec_Microsoft_EntityFramew_total_optimistic_concurrency_failures" - "ec_Microsoft_EntityF_optimistic_concurrency_failures_per_second" + +Aspire.RabbitMQ.Client: +- Log categories: + - "RabbitMQ.Client" +- Activity source names: + - "Aspire.RabbitMQ.Client" +- Metric names: + - none (currently not supported by RabbitMQ.Client library) + +Aspire.StackExchange.Redis: +- Log categories: + - "Aspire.StackExchange.Redis" (this name is defined by our component, we can change it) +- Activity source names: + - "OpenTelemetry.Instrumentation.StackExchangeRedis" +- Metric names: + - none (currently not supported by StackExchange.Redis library) + +Aspire.StackExchange.Redis.DistributedCaching: +- Everything from `Aspire.StackExchange.Redis` plus: +- Log categories: + - "Microsoft.Extensions.Caching.StackExchangeRedis" + +Aspire.StackExchange.Redis.OutputCaching: +- Everything from `Aspire.StackExchange.Redis` plus: +- Log categories: + - "Microsoft.AspNetCore.OutputCaching.StackExchangeRedis" diff --git a/tests/Aspire.Oracle.EntityFrameworkCore.Tests/ConformanceTests_Pooling.cs b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/ConformanceTests_Pooling.cs index 9c5e48ae77e..7136b51b65c 100644 --- a/tests/Aspire.Oracle.EntityFrameworkCore.Tests/ConformanceTests_Pooling.cs +++ b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/ConformanceTests_Pooling.cs @@ -90,12 +90,13 @@ protected override void TriggerActivity(TestDbContext service) [Theory] [InlineData(true)] [InlineData(false)] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "Required to verify pooling without touching DB")] public void DbContextPoolingRegistersIDbContextPool(bool enabled) { using IHost host = CreateHostWithComponent(options => options.DbContextPooling = enabled); +#pragma warning disable EF1001 // Internal EF Core API usage. IDbContextPool? pool = host.Services.GetService>(); +#pragma warning restore EF1001 Assert.Equal(enabled, pool is not null); } diff --git a/tests/testproject/TestProject.IntegrationServiceA/Oracle/MyDbContext.cs b/tests/testproject/TestProject.IntegrationServiceA/Oracle/MyDbContext.cs new file mode 100644 index 00000000000..0ff75373c8b --- /dev/null +++ b/tests/testproject/TestProject.IntegrationServiceA/Oracle/MyDbContext.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore; + +public class MyDbContext : DbContext +{ + public MyDbContext(DbContextOptions options) : base(options) + { + } +} diff --git a/tests/testproject/TestProject.IntegrationServiceA/Oracle/OracleDatabaseExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/Oracle/OracleDatabaseExtensions.cs index 208127eb1cd..43fb2c3f731 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/Oracle/OracleDatabaseExtensions.cs +++ b/tests/testproject/TestProject.IntegrationServiceA/Oracle/OracleDatabaseExtensions.cs @@ -1,26 +1,21 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Oracle.ManagedDataAccess.Client; +using Microsoft.EntityFrameworkCore; public static class OracleDatabaseExtensions { public static void MapOracleDatabaseApi(this WebApplication app) { - app.MapGet("/oracledatabase/verify", VerifyOracleDatabaseAsync); + app.MapGet("/oracledatabase/verify", VerifyOracleDatabase); } - private static async Task VerifyOracleDatabaseAsync(OracleConnection connection) + private static IResult VerifyOracleDatabase(MyDbContext context) { try { - await connection.OpenAsync(); - - var command = connection.CreateCommand(); - command.CommandText = $"SELECT 1 FROM DUAL"; - var results = await command.ExecuteReaderAsync(); - - return results.HasRows ? Results.Ok("Success!") : Results.Problem("Failed"); + var results = context.Database.SqlQueryRaw("SELECT 1 FROM DUAL"); + return results.Any() ? Results.Ok("Success!") : Results.Problem("Failed"); } catch (Exception e) { diff --git a/tests/testproject/TestProject.IntegrationServiceA/Oracle/OracleManagedDataAccessExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/Oracle/OracleManagedDataAccessExtensions.cs deleted file mode 100644 index 98e710e379b..00000000000 --- a/tests/testproject/TestProject.IntegrationServiceA/Oracle/OracleManagedDataAccessExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Oracle.ManagedDataAccess.Client; - -// This is a workaround while https://github.com/dotnet/aspire/pull/1004 is not merged. -public static class OracleManagedDataAccessExtensions -{ - public static void AddOracleClient(this WebApplicationBuilder builder, string connectionName) - { - ArgumentNullException.ThrowIfNull(builder); - - var connectionString = builder.Configuration.GetConnectionString(connectionName); - builder.Services.AddScoped(_ => new OracleConnection(connectionString)); - } - - public static void AddKeyedOracleClient(this WebApplicationBuilder builder, string connectionName) - { - ArgumentNullException.ThrowIfNull(builder); - - var connectionString = builder.Configuration.GetConnectionString(connectionName); - builder.Services.AddKeyedSingleton(connectionName, (serviceProvider, _) => new OracleConnection(connectionString)); - } -} diff --git a/tests/testproject/TestProject.IntegrationServiceA/Program.cs b/tests/testproject/TestProject.IntegrationServiceA/Program.cs index a4d7784dcf6..ce1f4803caa 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/Program.cs +++ b/tests/testproject/TestProject.IntegrationServiceA/Program.cs @@ -8,7 +8,7 @@ builder.AddNpgsqlDataSource("postgresdb"); builder.AddRabbitMQ("rabbitmqcontainer"); builder.AddMongoDBClient("mymongodb"); -builder.AddOracleClient("freepdb1"); +builder.AddOracleDatabaseDbContext("freepdb1"); builder.AddKeyedSqlServerClient("sqlserverabstract"); builder.AddKeyedMySqlDataSource("mysqlabstract"); @@ -16,7 +16,6 @@ builder.AddKeyedNpgsqlDataSource("postgresabstract"); builder.AddKeyedRabbitMQ("rabbitmqabstract"); builder.AddKeyedMongoDBClient("mongodbabstract"); -builder.AddKeyedOracleClient("oracledatabaseabstract"); var app = builder.Build(); From 5d99c707b64cd5a330674a57816ee5c63594a1a0 Mon Sep 17 00:00:00 2001 From: Andre Lins Date: Sat, 16 Dec 2023 20:44:30 -0300 Subject: [PATCH 10/12] configurationschema was outdated --- .../Aspire.Oracle.EntityFrameworkCore/ConfigurationSchema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Aspire.Oracle.EntityFrameworkCore/ConfigurationSchema.json b/src/Components/Aspire.Oracle.EntityFrameworkCore/ConfigurationSchema.json index 56695f80e01..0f53b36394a 100644 --- a/src/Components/Aspire.Oracle.EntityFrameworkCore/ConfigurationSchema.json +++ b/src/Components/Aspire.Oracle.EntityFrameworkCore/ConfigurationSchema.json @@ -57,7 +57,7 @@ }, "DbContextPooling": { "type": "boolean", - "description": "Gets or sets a boolean value that indicates whether the db context will be pooled or explicitly created every time it\u0027s requested." + "description": "Gets or sets a boolean value that indicates whether the db context will be pooled or explicitly created every time it's requested." }, "HealthChecks": { "type": "boolean", From 7c09cbde88740839215c747ffecbfb282902e325 Mon Sep 17 00:00:00 2001 From: Andre Lins Date: Tue, 2 Jan 2024 20:06:32 -0300 Subject: [PATCH 11/12] adjusting to reviews --- Aspire.sln | 28 ++++++------------- ...ce.cs => IOracleDatabaseParentResource.cs} | 0 .../Oracle/OracleDatabaseContainerResource.cs | 4 ++- .../README.md | 4 +-- .../Oracle/OracleDatabaseFunctionalTests.cs | 3 +- 5 files changed, 15 insertions(+), 24 deletions(-) rename src/Aspire.Hosting/Oracle/{IOracleDatabaseResource.cs => IOracleDatabaseParentResource.cs} (100%) diff --git a/Aspire.sln b/Aspire.sln index ba72c960ed6..3fd55c66a03 100644 --- a/Aspire.sln +++ b/Aspire.sln @@ -170,11 +170,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.MongoDB.Driver", "sr EndProject 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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestProject.LaunchSettings", "tests\testproject\TestProject.LaunchSettings\TestProject.LaunchSettings.csproj", "{A734177E-213B-4D68-98A4-6F5C00234053}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConfigurationSchemaGenerator.Tests", "tests\ConfigurationSchemaGenerator.Tests\ConfigurationSchemaGenerator.Tests.csproj", "{00FEA181-84C9-42A7-AC81-29A9F176A1A0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Oracle.EntityFrameworkCore", "src\Components\Aspire.Oracle.EntityFrameworkCore\Aspire.Oracle.EntityFrameworkCore.csproj", "{A778F29A-6C40-4C53-A793-F23F20679ADE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Oracle.EntityFrameworkCore.Tests", "tests\Aspire.Oracle.EntityFrameworkCore.Tests\Aspire.Oracle.EntityFrameworkCore.Tests.csproj", "{A331C123-35A5-4E81-9999-354159821374}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Oracle.EntityFrameworkCore.Tests", "tests\Aspire.Oracle.EntityFrameworkCore.Tests\Aspire.Oracle.EntityFrameworkCore.Tests.csproj", "{A331C123-35A5-4E81-9999-354159821374}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -458,10 +458,10 @@ 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 - {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 + {00FEA181-84C9-42A7-AC81-29A9F176A1A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00FEA181-84C9-42A7-AC81-29A9F176A1A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00FEA181-84C9-42A7-AC81-29A9F176A1A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00FEA181-84C9-42A7-AC81-29A9F176A1A0}.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 @@ -470,14 +470,6 @@ Global {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 - {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 - {39FA2A64-012F-4EB9-A14F-E8AC54C975F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {39FA2A64-012F-4EB9-A14F-E8AC54C975F6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {39FA2A64-012F-4EB9-A14F-E8AC54C975F6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {39FA2A64-012F-4EB9-A14F-E8AC54C975F6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -553,16 +545,12 @@ Global {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} - {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} - {DCF2D47A-921A-4900-B5B2-CF97B3531CE8} = {975F6F41-B455-451D-A312-098DE4A167B6} {39FA2A64-012F-4EB9-A14F-E8AC54C975F6} = {2136E31D-2CBB-41BB-8618-716FF8E46E9E} {20A5A907-A135-4735-B4BF-E13514F360E3} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2} {E592E447-BA3C-44FA-86C1-EBEDC864A644} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60} {00FEA181-84C9-42A7-AC81-29A9F176A1A0} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60} + {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} diff --git a/src/Aspire.Hosting/Oracle/IOracleDatabaseResource.cs b/src/Aspire.Hosting/Oracle/IOracleDatabaseParentResource.cs similarity index 100% rename from src/Aspire.Hosting/Oracle/IOracleDatabaseResource.cs rename to src/Aspire.Hosting/Oracle/IOracleDatabaseParentResource.cs diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseContainerResource.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseContainerResource.cs index 76e697219e7..a41366cb602 100644 --- a/src/Aspire.Hosting/Oracle/OracleDatabaseContainerResource.cs +++ b/src/Aspire.Hosting/Oracle/OracleDatabaseContainerResource.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Aspire.Hosting.Utils; + namespace Aspire.Hosting.ApplicationModel; /// @@ -25,7 +27,7 @@ public class OracleDatabaseContainerResource(string name, string password) : Con var allocatedEndpoint = allocatedEndpoints.Single(); // We should only have one endpoint for Oracle Database. - var connectionString = $"user id=system;password={Password};data source={allocatedEndpoint.Address}:{allocatedEndpoint.Port}"; + var connectionString = $"user id=system;password={PasswordUtil.EscapePassword(Password)};data source={allocatedEndpoint.Address}:{allocatedEndpoint.Port}"; return connectionString; } } diff --git a/src/Components/Aspire.Oracle.EntityFrameworkCore/README.md b/src/Components/Aspire.Oracle.EntityFrameworkCore/README.md index 3c46bcc28f4..428ab17d95e 100644 --- a/src/Components/Aspire.Oracle.EntityFrameworkCore/README.md +++ b/src/Components/Aspire.Oracle.EntityFrameworkCore/README.md @@ -57,7 +57,7 @@ And then the connection string will be retrieved from the `ConnectionStrings` co } ``` -See the [ODP.NET documentation](https://docs.oracle.com/en/database/oracle/oracle-database/21/odpnt/#Oracle%C2%AE-Data-Provider-for-.NET) for more information on how to format this connection string. +See the [ODP.NET documentation](https://www.oracle.com/database/technologies/appdev/dotnet/odp.html) for more information on how to format this connection string. ### Use configuration providers @@ -91,7 +91,7 @@ Also you can pass the `Action configureSettin In your AppHost project, register an Oracle container and consume the connection using the following methods: ```csharp - var freepdb1 = builder.AddOracleDatabaseContainer("oracle").AddDatabase("freepdb1"); + var freepdb1 = builder.AddOracleDatabase("oracle").AddDatabase("freepdb1"); var myService = builder.AddProject() .WithReference(freepdb1); diff --git a/tests/Aspire.Hosting.Tests/Oracle/OracleDatabaseFunctionalTests.cs b/tests/Aspire.Hosting.Tests/Oracle/OracleDatabaseFunctionalTests.cs index c2c026cb9e9..570cbf805a7 100644 --- a/tests/Aspire.Hosting.Tests/Oracle/OracleDatabaseFunctionalTests.cs +++ b/tests/Aspire.Hosting.Tests/Oracle/OracleDatabaseFunctionalTests.cs @@ -25,7 +25,8 @@ public async Task VerifyOracleDatabaseWorks() using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/oracledatabase/verify", cts.Token); + var responseContent = await response.Content.ReadAsStringAsync(); - Assert.True(response.IsSuccessStatusCode); + Assert.True(response.IsSuccessStatusCode, responseContent); } } From 5f0b689bb19f4e8a484b6feff2d9a1de883a9f10 Mon Sep 17 00:00:00 2001 From: Andre Lins Date: Tue, 2 Jan 2024 20:36:25 -0300 Subject: [PATCH 12/12] updated with the latest bits --- .../Oracle/OracleDatabaseBuilderExtensions.cs | 4 ++-- tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs index f8191b21885..4aa5775a73c 100644 --- a/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs +++ b/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs @@ -28,7 +28,7 @@ public static IResourceBuilder AddOracleDatabas var oracleDatabaseContainer = new OracleDatabaseContainerResource(name, password); return builder.AddResource(oracleDatabaseContainer) .WithManifestPublishingCallback(context => WriteOracleDatabaseContainerResourceToManifest(context, oracleDatabaseContainer)) - .WithAnnotation(new ServiceBindingAnnotation(ProtocolType.Tcp, port: port, containerPort: 1521)) + .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: port, containerPort: 1521)) .WithAnnotation(new ContainerImageAnnotation { Image = "database/free", Tag = "latest", Registry = "container-registry.oracle.com" }) .WithEnvironment(context => { @@ -55,7 +55,7 @@ public static IResourceBuilder AddOracleDatabase(t var oracleDatabaseServer = new OracleDatabaseServerResource(name, password); return builder.AddResource(oracleDatabaseServer) .WithManifestPublishingCallback(WriteOracleDatabaseContainerToManifest) - .WithAnnotation(new ServiceBindingAnnotation(ProtocolType.Tcp, containerPort: 1521)) + .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, containerPort: 1521)) .WithAnnotation(new ContainerImageAnnotation { Image = "database/free", Tag = "latest", Registry = "container-registry.oracle.com" }) .WithEnvironment(PasswordEnvVarName, () => oracleDatabaseServer.Password); } diff --git a/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs b/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs index 035d1613f60..eaf7e13a7e6 100644 --- a/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs +++ b/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs @@ -30,7 +30,7 @@ public void AddOracleDatabaseWithDefaultsAddsAnnotationMetadata() Assert.Equal("database/free", containerAnnotation.Image); Assert.Equal("container-registry.oracle.com", containerAnnotation.Registry); - var serviceBinding = Assert.Single(containerResource.Annotations.OfType()); + var serviceBinding = Assert.Single(containerResource.Annotations.OfType()); Assert.Equal(1521, serviceBinding.ContainerPort); Assert.False(serviceBinding.IsExternal); Assert.Equal("tcp", serviceBinding.Name); @@ -78,7 +78,7 @@ public void AddOracleDatabaseAddsAnnotationMetadata() Assert.Equal("database/free", containerAnnotation.Image); Assert.Equal("container-registry.oracle.com", containerAnnotation.Registry); - var serviceBinding = Assert.Single(containerResource.Annotations.OfType()); + var serviceBinding = Assert.Single(containerResource.Annotations.OfType()); Assert.Equal(1521, serviceBinding.ContainerPort); Assert.False(serviceBinding.IsExternal); Assert.Equal("tcp", serviceBinding.Name); @@ -177,7 +177,7 @@ public void AddDatabaseToOracleDatabaseAddsAnnotationMetadata() Assert.Equal("database/free", containerAnnotation.Image); Assert.Equal("container-registry.oracle.com", containerAnnotation.Registry); - var serviceBinding = Assert.Single(containerResource.Annotations.OfType()); + var serviceBinding = Assert.Single(containerResource.Annotations.OfType()); Assert.Equal(1521, serviceBinding.ContainerPort); Assert.False(serviceBinding.IsExternal); Assert.Equal("tcp", serviceBinding.Name);