diff --git a/Aspire.sln b/Aspire.sln
index 305c92b64ad..3fd55c66a03 100644
--- a/Aspire.sln
+++ b/Aspire.sln
@@ -172,6 +172,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.MongoDB.Driver.Tests
EndProject
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("{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
Debug|Any CPU = Debug|Any CPU
@@ -458,6 +462,14 @@ Global
{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
+ {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
@@ -537,6 +549,8 @@ Global
{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/Directory.Packages.props b/Directory.Packages.props
index d66e980bb6d..f18f6ff3b47 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -79,6 +79,7 @@
+
@@ -105,4 +106,4 @@
-
+
\ No newline at end of file
diff --git a/src/Aspire.Hosting/Oracle/IOracleDatabaseParentResource.cs b/src/Aspire.Hosting/Oracle/IOracleDatabaseParentResource.cs
new file mode 100644
index 00000000000..d5b63d1d050
--- /dev/null
+++ b/src/Aspire.Hosting/Oracle/IOracleDatabaseParentResource.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 IOracleDatabaseParentResource : IResourceWithConnectionString, IResourceWithEnvironment
+{
+}
diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs
new file mode 100644
index 00000000000..4aa5775a73c
--- /dev/null
+++ b/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs
@@ -0,0 +1,105 @@
+// 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_PWD";
+
+ ///
+ /// 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").Substring(0, 30);
+ var oracleDatabaseContainer = new OracleDatabaseContainerResource(name, password);
+ return builder.AddResource(oracleDatabaseContainer)
+ .WithManifestPublishingCallback(context => WriteOracleDatabaseContainerResourceToManifest(context, oracleDatabaseContainer))
+ .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: port, containerPort: 1521))
+ .WithAnnotation(new ContainerImageAnnotation { Image = "database/free", Tag = "latest", Registry = "container-registry.oracle.com" })
+ .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 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.
+ /// A reference to the .
+ public static IResourceBuilder AddOracleDatabase(this IDistributedApplicationBuilder builder, string name)
+ {
+ var password = Guid.NewGuid().ToString("N").Substring(0, 30);
+ var oracleDatabaseServer = new OracleDatabaseServerResource(name, password);
+ return builder.AddResource(oracleDatabaseServer)
+ .WithManifestPublishingCallback(WriteOracleDatabaseContainerToManifest)
+ .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, containerPort: 1521))
+ .WithAnnotation(new ContainerImageAnnotation { Image = "database/free", Tag = "latest", Registry = "container-registry.oracle.com" })
+ .WithEnvironment(PasswordEnvVarName, () => oracleDatabaseServer.Password);
+ }
+
+ ///
+ /// 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)
+ .WithManifestPublishingCallback(context => WriteOracleDatabaseToManifest(context, oracleDatabase));
+ }
+
+ 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);
+ }
+
+ 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/OracleDatabaseContainerResource.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseContainerResource.cs
new file mode 100644
index 00000000000..a41366cb602
--- /dev/null
+++ b/src/Aspire.Hosting/Oracle/OracleDatabaseContainerResource.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 OracleDatabaseContainerResource(string name, string password) : ContainerResource(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=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={PasswordUtil.EscapePassword(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..0f9e1dfa271
--- /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 parent resource associated with this database.
+public class OracleDatabaseResource(string name, IOracleDatabaseParentResource oracleParentResource) : Resource(name), IResourceWithParent, IResourceWithConnectionString
+{
+ public IOracleDatabaseParentResource Parent { get; } = oracleParentResource;
+
+ ///
+ /// 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.");
+ }
+ }
+}
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;
+ }
+}
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..37943f18e42
--- /dev/null
+++ b/src/Components/Aspire.Oracle.EntityFrameworkCore/Aspire.Oracle.EntityFrameworkCore.csproj
@@ -0,0 +1,25 @@
+
+
+
+ $(NetCurrent)
+ true
+ $(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/AspireOracleEFCoreExtensions.cs b/src/Components/Aspire.Oracle.EntityFrameworkCore/AspireOracleEFCoreExtensions.cs
new file mode 100644
index 00000000000..1a30234899d
--- /dev/null
+++ b/src/Components/Aspire.Oracle.EntityFrameworkCore/AspireOracleEFCoreExtensions.cs
@@ -0,0 +1,125 @@
+// 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;
+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 AspireOracleEFCoreExtensions
+{
+ private const string DefaultConfigSectionName = "Aspire:Oracle:EntityFrameworkCore";
+ 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:{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.
+ public static void AddOracleDatabaseDbContext<[DynamicallyAccessedMembers(RequiredByEF)] TContext>(
+ this IHostApplicationBuilder builder,
+ string connectionName,
+ Action? configureSettings = null,
+ Action? configureDbContextOptions = null) where TContext : DbContext
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ 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
+ {
+ 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 =>
+ {
+ // https://github.com/dotnet/efcore/blob/a1cd4f45aa18314bc91d2b9ea1f71a3b7d5bf636/src/EFCore/Infrastructure/EntityFrameworkEventSource.cs#L45
+ eventCountersInstrumentationOptions.AddEventSources("Microsoft.EntityFrameworkCore");
+ });
+ });
+ }
+
+ 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/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
new file mode 100644
index 00000000000..0f53b36394a
--- /dev/null
+++ b/src/Components/Aspire.Oracle.EntityFrameworkCore/ConfigurationSchema.json
@@ -0,0 +1,91 @@
+{
+ "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.Connection": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "Microsoft.EntityFrameworkCore.Database.Transaction": {
+ "$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": "The connection string of the Oracle database to connect to."
+ },
+ "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's requested."
+ },
+ "HealthChecks": {
+ "type": "boolean",
+ "description": "Gets or sets a boolean value that indicates whether the database health check is enabled or not. The default value is 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 Open Telemetry metrics are enabled or not. The default value is true."
+ },
+ "Timeout": {
+ "type": "integer",
+ "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."
+ }
+ }
+ }
+ }
+ }
+ },
+ "type": "object"
+}
diff --git a/src/Components/Aspire.Oracle.EntityFrameworkCore/OracleEntityFrameworkCoreSettings.cs b/src/Components/Aspire.Oracle.EntityFrameworkCore/OracleEntityFrameworkCoreSettings.cs
new file mode 100644
index 00000000000..6c869a873e8
--- /dev/null
+++ b/src/Components/Aspire.Oracle.EntityFrameworkCore/OracleEntityFrameworkCoreSettings.cs
@@ -0,0 +1,50 @@
+// 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;
+
+///
+/// Provides the client configuration settings for connecting to a Oracle database using EntityFrameworkCore.
+///
+public sealed class OracleEntityFrameworkCoreSettings
+{
+ ///
+ /// 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/README.md b/src/Components/Aspire.Oracle.EntityFrameworkCore/README.md
new file mode 100644
index 00000000000..428ab17d95e
--- /dev/null
+++ b/src/Components/Aspire.Oracle.EntityFrameworkCore/README.md
@@ -0,0 +1,113 @@
+# 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.
+
+## Getting started
+
+### Prerequisites
+
+- Oracle database and connection string for accessing the database.
+
+### Install the package
+
+Install the .NET Aspire Oracle EntityFrameworkCore library with [NuGet](https://www.nuget.org):
+
+```dotnetcli
+dotnet add package Aspire.Oracle.EntityFrameworkCore
+```
+
+## 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 component provides multiple options to configure the database 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://www.oracle.com/database/technologies/appdev/dotnet/odp.html) for more information on how to format this connection string.
+
+### Use configuration providers
+
+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": {
+ "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);
+```
+
+## AppHost extensions
+
+ In your AppHost project, register an Oracle container and consume the connection using the following methods:
+
+ ```csharp
+ var freepdb1 = builder.AddOracleDatabase("oracle").AddDatabase("freepdb1");
+
+ var myService = builder.AddProject()
+ .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/
+* 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..beec376e8f9 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 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Nomenclature used in the table above:
diff --git a/src/Components/Telemetry.md b/src/Components/Telemetry.md
index f5b76e3e90d..cb8df79853b 100644
--- a/src/Components/Telemetry.md
+++ b/src/Components/Telemetry.md
@@ -223,6 +223,33 @@ Aspire.Npgsql.EntityFrameworkCore.PostgreSQL:
- "db.client.connections.timeouts"
- "db.client.connections.usage"
+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"
+
Aspire.RabbitMQ.Client:
- Log categories:
- "RabbitMQ.Client"
diff --git a/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs b/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs
new file mode 100644
index 00000000000..eaf7e13a7e6
--- /dev/null
+++ b/tests/Aspire.Hosting.Tests/Oracle/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.Oracle;
+
+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.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/Aspire.Hosting.Tests/Oracle/OracleDatabaseFunctionalTests.cs b/tests/Aspire.Hosting.Tests/Oracle/OracleDatabaseFunctionalTests.cs
new file mode 100644
index 00000000000..570cbf805a7
--- /dev/null
+++ b/tests/Aspire.Hosting.Tests/Oracle/OracleDatabaseFunctionalTests.cs
@@ -0,0 +1,32 @@
+// 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);
+ var responseContent = await response.Content.ReadAsStringAsync();
+
+ Assert.True(response.IsSuccessStatusCode, responseContent);
+ }
+}
diff --git a/tests/Aspire.Oracle.EntityFrameworkCore.Tests/Aspire.Oracle.EntityFrameworkCore.Tests.csproj b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/Aspire.Oracle.EntityFrameworkCore.Tests.csproj
new file mode 100644
index 00000000000..26e5a5b30ed
--- /dev/null
+++ b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/Aspire.Oracle.EntityFrameworkCore.Tests.csproj
@@ -0,0 +1,17 @@
+
+
+
+ $(NetCurrent)
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Aspire.Oracle.EntityFrameworkCore.Tests/AspireOracleEFCoreDatabaseExtensionsTests.cs b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/AspireOracleEFCoreDatabaseExtensionsTests.cs
new file mode 100644
index 00000000000..c7756e07a66
--- /dev/null
+++ b/tests/Aspire.Oracle.EntityFrameworkCore.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.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: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:MaxRetryCount", "304"),
+ new KeyValuePair("Aspire:Oracle:EntityFrameworkCore: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.Tests/ConformanceTests_NoPooling.cs b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/ConformanceTests_NoPooling.cs
new file mode 100644
index 00000000000..a26f7e14d98
--- /dev/null
+++ b/tests/Aspire.Oracle.EntityFrameworkCore.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.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.Tests/ConformanceTests_NoPooling_TypeSpecificConfig.cs b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/ConformanceTests_NoPooling_TypeSpecificConfig.cs
new file mode 100644
index 00000000000..f4191ebc303
--- /dev/null
+++ b/tests/Aspire.Oracle.EntityFrameworkCore.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.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:{typeof(TestDbContext).Name}:ConnectionString", ConnectionString)
+ });
+}
diff --git a/tests/Aspire.Oracle.EntityFrameworkCore.Tests/ConformanceTests_Pooling.cs b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/ConformanceTests_Pooling.cs
new file mode 100644
index 00000000000..7136b51b65c
--- /dev/null
+++ b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/ConformanceTests_Pooling.cs
@@ -0,0 +1,123 @@
+// 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.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/ConfigurationSchema.json";
+
+ protected override string ValidJsonConfig => """
+ {
+ "Aspire": {
+ "Oracle": {
+ "EntityFrameworkCore": {
+ "ConnectionString": "YOUR_CONNECTION_STRING",
+ "HealthChecks": false,
+ "DbContextPooling": true,
+ "Tracing": true,
+ "Metrics": true
+ }
+ }
+ }
+ }
+ """;
+
+ protected override (string json, string error)[] InvalidJsonToErrorMessage => new[]
+ {
+ ("""{"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:ConnectionString", ConnectionString)
+ });
+
+ protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null)
+ => builder.AddOracleDatabaseDbContext("orclconnection", configure);
+
+ protected override void SetHealthCheck(OracleEntityFrameworkCoreSettings options, bool enabled)
+ => options.HealthChecks = enabled;
+
+ protected override void SetTracing(OracleEntityFrameworkCoreSettings options, bool enabled)
+ => options.Tracing = enabled;
+
+ protected override void SetMetrics(OracleEntityFrameworkCoreSettings options, bool enabled)
+ => options.Metrics = enabled;
+
+ protected override void TriggerActivity(TestDbContext service)
+ {
+ if (service.Database.CanConnect())
+ {
+ service.Database.EnsureCreated();
+ }
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ 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);
+ }
+
+ [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.Tests/ConformanceTests_Pooling_TypeSpecificConfig.cs b/tests/Aspire.Oracle.EntityFrameworkCore.Tests/ConformanceTests_Pooling_TypeSpecificConfig.cs
new file mode 100644
index 00000000000..c75e7d7d9a6
--- /dev/null
+++ b/tests/Aspire.Oracle.EntityFrameworkCore.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.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:{typeof(TestDbContext).Name}:ConnectionString", ConnectionString)
+ });
+}
diff --git a/tests/testproject/TestProject.AppHost/TestProgram.cs b/tests/testproject/TestProject.AppHost/TestProgram.cs
index 5e7ef5945ec..c4902b06984 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/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
new file mode 100644
index 00000000000..43fb2c3f731
--- /dev/null
+++ b/tests/testproject/TestProject.IntegrationServiceA/Oracle/OracleDatabaseExtensions.cs
@@ -0,0 +1,25 @@
+// 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 static class OracleDatabaseExtensions
+{
+ public static void MapOracleDatabaseApi(this WebApplication app)
+ {
+ app.MapGet("/oracledatabase/verify", VerifyOracleDatabase);
+ }
+
+ private static IResult VerifyOracleDatabase(MyDbContext context)
+ {
+ try
+ {
+ var results = context.Database.SqlQueryRaw("SELECT 1 FROM DUAL");
+ return results.Any() ? Results.Ok("Success!") : Results.Problem("Failed");
+ }
+ catch (Exception e)
+ {
+ return Results.Problem(e.ToString());
+ }
+ }
+}
diff --git a/tests/testproject/TestProject.IntegrationServiceA/Program.cs b/tests/testproject/TestProject.IntegrationServiceA/Program.cs
index b809f3e7d84..ce1f4803caa 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.AddOracleDatabaseDbContext("freepdb1");
builder.AddKeyedSqlServerClient("sqlserverabstract");
builder.AddKeyedMySqlDataSource("mysqlabstract");
@@ -36,4 +37,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 45a3272abcc..4b8406eeada 100644
--- a/tests/testproject/TestProject.IntegrationServiceA/TestProject.IntegrationServiceA.csproj
+++ b/tests/testproject/TestProject.IntegrationServiceA/TestProject.IntegrationServiceA.csproj
@@ -14,6 +14,7 @@
+