diff --git a/Aspire.sln b/Aspire.sln
index b9e012682bd..29a1ad40c41 100644
--- a/Aspire.sln
+++ b/Aspire.sln
@@ -152,7 +152,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.RabbitMQ.Client", "s
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.RabbitMQ.Client.Tests", "tests\Aspire.RabbitMQ.Client.Tests\Aspire.RabbitMQ.Client.Tests.csproj", "{165411FE-755E-4869-A756-F87F455860AC}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject.WorkerA", "tests\testproject\TestProject.WorkerA\TestProject.WorkerA.csproj", "{6472D59F-7C04-43DE-AD33-9F20BE3804BF}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestProject.WorkerA", "tests\testproject\TestProject.WorkerA\TestProject.WorkerA.csproj", "{6472D59F-7C04-43DE-AD33-9F20BE3804BF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.MySqlConnector", "src\Components\Aspire.MySqlConnector\Aspire.MySqlConnector.csproj", "{CA283D7F-EB95-4353-B196-C409965D2B42}"
EndProject
@@ -160,6 +160,10 @@ 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.Oracle.ManagedDataAccess.Core", "src\Components\Aspire.Oracle.ManagedDataAccess.Core\Aspire.Oracle.ManagedDataAccess.Core.csproj", "{B13DF73F-A22F-4811-861B-4BC464A0D9F3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Oracle.ManagedDataAccess.Core.Tests", "tests\Aspire.Oracle.ManagedDataAccess.Core.Tests\Aspire.Oracle.ManagedDataAccess.Core.Tests.csproj", "{D696C06F-AEFE-429D-A97A-AF3F4D4847D7}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -414,6 +418,10 @@ Global
{165411FE-755E-4869-A756-F87F455860AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{165411FE-755E-4869-A756-F87F455860AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{165411FE-755E-4869-A756-F87F455860AC}.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
{CA283D7F-EB95-4353-B196-C409965D2B42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CA283D7F-EB95-4353-B196-C409965D2B42}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CA283D7F-EB95-4353-B196-C409965D2B42}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -426,10 +434,14 @@ Global
{DCF2D47A-921A-4900-B5B2-CF97B3531CE8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DCF2D47A-921A-4900-B5B2-CF97B3531CE8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DCF2D47A-921A-4900-B5B2-CF97B3531CE8}.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
+ {B13DF73F-A22F-4811-861B-4BC464A0D9F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B13DF73F-A22F-4811-861B-4BC464A0D9F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B13DF73F-A22F-4811-861B-4BC464A0D9F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B13DF73F-A22F-4811-861B-4BC464A0D9F3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D696C06F-AEFE-429D-A97A-AF3F4D4847D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D696C06F-AEFE-429D-A97A-AF3F4D4847D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D696C06F-AEFE-429D-A97A-AF3F4D4847D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D696C06F-AEFE-429D-A97A-AF3F4D4847D7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -501,10 +513,12 @@ Global
{A84C4EE3-2601-4804-BCDC-E9948E164A22} = {A68BA1A5-1604-433D-9778-DC0199831C2A}
{4D8A92AB-4E77-4965-AD8E-8E206DCE66A4} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2}
{165411FE-755E-4869-A756-F87F455860AC} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
+ {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}
- {6472D59F-7C04-43DE-AD33-9F20BE3804BF} = {975F6F41-B455-451D-A312-098DE4A167B6}
+ {B13DF73F-A22F-4811-861B-4BC464A0D9F3} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2}
+ {D696C06F-AEFE-429D-A97A-AF3F4D4847D7} = {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 521516e0e83..a577f439900 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -40,6 +40,7 @@
+
@@ -74,6 +75,7 @@
+
@@ -93,6 +95,7 @@
+
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..08c1f6e9ef8
--- /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 System.Text.Json;
+using Aspire.Hosting.ApplicationModel;
+
+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((json) => WriteOracleDatabaseConnectionToManifest(json, 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(Utf8JsonWriter jsonWriter, OracleDatabaseConnectionResource oracleDatabaseConnection)
+ {
+ jsonWriter.WriteString("type", "oracle.connection.v0");
+ jsonWriter.WriteString("connectionString", oracleDatabaseConnection.GetConnectionString());
+ }
+
+ private static void WriteOracleDatabaseContainerToManifest(Utf8JsonWriter jsonWriter)
+ {
+ jsonWriter.WriteString("type", "oracle.server.v0");
+ }
+
+ private static void WriteOracleDatabaseToManifest(Utf8JsonWriter json, OracleDatabaseResource oracleDatabase)
+ {
+ json.WriteString("type", "oracle.database.v0");
+ json.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.");
+ }
+ }
+}
diff --git a/src/Components/Aspire.Oracle.ManagedDataAccess.Core/Aspire.Oracle.ManagedDataAccess.Core.csproj b/src/Components/Aspire.Oracle.ManagedDataAccess.Core/Aspire.Oracle.ManagedDataAccess.Core.csproj
new file mode 100644
index 00000000000..0a0578f29f7
--- /dev/null
+++ b/src/Components/Aspire.Oracle.ManagedDataAccess.Core/Aspire.Oracle.ManagedDataAccess.Core.csproj
@@ -0,0 +1,24 @@
+
+
+
+ $(NetCurrent)
+ true
+ $(ComponentDatabasePackageTags) oracle odp managed data access core sql
+ A Oracle® client that integrates with Aspire, including health checks, metrics, logging, and telemetry.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Components/Aspire.Oracle.ManagedDataAccess.Core/AspireOracleManagedDataAccessCoreExtensions.cs b/src/Components/Aspire.Oracle.ManagedDataAccess.Core/AspireOracleManagedDataAccessCoreExtensions.cs
new file mode 100644
index 00000000000..5f1ab2a5cc2
--- /dev/null
+++ b/src/Components/Aspire.Oracle.ManagedDataAccess.Core/AspireOracleManagedDataAccessCoreExtensions.cs
@@ -0,0 +1,131 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire;
+using Aspire.Oracle.ManagedDataAccess.Core;
+using HealthChecks.Oracle;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Logging;
+using Oracle.ManagedDataAccess.Client;
+using Oracle.ManagedDataAccess.OpenTelemetry;
+
+namespace Microsoft.Extensions.Hosting;
+
+///
+/// Extension methods for connecting Oracle database with Oracle.ManagedDataAccess.Core client
+///
+public static class AspireOracleManagedDataAccessCoreExtensions
+{
+ private const string DefaultConfigSectionName = "Aspire:Oracle:ManagedDataAccess:Core";
+
+ ///
+ /// Registers service for connecting Oracle database with Oracle.ManagedDataAccess.Core client.
+ /// Configures health check and telemetry for the Oracle.ManagedDataAccess.Core client.
+ ///
+ /// 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.
+ /// Reads the configuration from "Aspire:Oracle:ManagedDataAccess:Core" section.
+ /// Thrown if mandatory is null.
+ /// Thrown when mandatory is not provided.
+ public static void AddOracleManagedDataAccessCore(this IHostApplicationBuilder builder, string connectionName, Action? configureSettings = null)
+ => AddOracleManagedDataAccessCore(builder, DefaultConfigSectionName, configureSettings, connectionName, serviceKey: null);
+
+ ///
+ /// Registers as a keyed service for given for connecting Oracle database with Oracle.ManagedDataAccess.Core client.
+ /// Configures health check and telemetry for the Npgsql client.
+ ///
+ /// The to read config from and add services to.
+ /// The name of the component, which is used as the of the service and also to retrieve the connection string from the ConnectionStrings configuration section.
+ /// An optional method that can be used for customizing options. It's invoked after the settings are read from the configuration.
+ /// Reads the configuration from "Aspire:Oracle:ManagedDataAccess:Core:{name}" section.
+ /// Thrown when or is null.
+ /// Thrown if mandatory is empty.
+ /// Thrown when mandatory is not provided.
+ public static void AddKeyedOracleManagedDataAccessCore(this IHostApplicationBuilder builder, string name, Action? configureSettings = null)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(name);
+
+ AddOracleManagedDataAccessCore(builder, $"{DefaultConfigSectionName}:{name}", configureSettings, connectionName: name, serviceKey: name);
+ }
+
+ private static void AddOracleManagedDataAccessCore(IHostApplicationBuilder builder, string configurationSectionName,
+ Action? configureSettings, string connectionName, object? serviceKey)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ OracleManagedDataAccessCoreSettings settings = new();
+ builder.Configuration.GetSection(configurationSectionName).Bind(settings);
+
+ if (builder.Configuration.GetConnectionString(connectionName) is string connectionString)
+ {
+ settings.ConnectionString = connectionString;
+ }
+
+ configureSettings?.Invoke(settings);
+
+ builder.RegisterOracleManagedDataAccessCoreServices(settings, configurationSectionName, connectionName, serviceKey);
+
+ if (settings.HealthChecks)
+ {
+ builder.TryAddHealthCheck(new HealthCheckRegistration(
+ serviceKey is null ? "OracleManagedDataAccessCore" : $"OracleManagedDataAccessCore_{connectionName}",
+ sp => new OracleHealthCheck(new OracleHealthCheckOptions()
+ {
+ ConnectionString = settings.ConnectionString ?? string.Empty
+ }),
+ failureStatus: default,
+ tags: default,
+ timeout: default));
+ }
+
+ if (settings.Tracing)
+ {
+ builder.Services.AddOpenTelemetry()
+ .WithTracing(tracerProviderBuilder =>
+ {
+ tracerProviderBuilder.AddOracleDataProviderInstrumentation(o =>
+ {
+ o.EnableConnectionLevelAttributes = true;
+ o.RecordException = true;
+ o.InstrumentOracleDataReaderRead = true;
+ o.SetDbStatementForText = true;
+ })
+ .AddSource("Oracle.ManagedDataAccess.Core");
+ });
+ }
+ }
+
+ private static void RegisterOracleManagedDataAccessCoreServices(this IHostApplicationBuilder builder, OracleManagedDataAccessCoreSettings settings, string configurationSectionName, string connectionName, object? serviceKey)
+ {
+ if (serviceKey is null)
+ {
+ // delay validating the ConnectionString until the DataSource is requested. This ensures an exception doesn't happen until a Logger is established.
+ builder.Services.AddScoped(serviceProvider =>
+ {
+ ValidateConnection();
+
+ return new OracleConnection(settings.ConnectionString);
+ });
+ }
+ else
+ {
+ builder.Services.AddKeyedScoped(serviceKey, (serviceProvider, key) =>
+ {
+ ValidateConnection();
+
+ return new OracleConnection(settings.ConnectionString);
+ });
+ }
+
+ void ValidateConnection()
+ {
+ if (string.IsNullOrEmpty(settings.ConnectionString))
+ {
+ throw new InvalidOperationException($"ConnectionString is missing. It should be provided in 'ConnectionStrings:{connectionName}' or under the 'ConnectionString' key in '{configurationSectionName}' configuration section.");
+ }
+ }
+ }
+}
diff --git a/src/Components/Aspire.Oracle.ManagedDataAccess.Core/ConfigurationSchema.json b/src/Components/Aspire.Oracle.ManagedDataAccess.Core/ConfigurationSchema.json
new file mode 100644
index 00000000000..8b05f3fea98
--- /dev/null
+++ b/src/Components/Aspire.Oracle.ManagedDataAccess.Core/ConfigurationSchema.json
@@ -0,0 +1,39 @@
+{
+ "properties": {
+ "Aspire": {
+ "type": "object",
+ "properties": {
+ "Oracle": {
+ "type": "object",
+ "properties": {
+ "ManagedDataAccess": {
+ "type": "object",
+ "properties": {
+ "Core": {
+ "type": "object",
+ "properties": {
+ "ConnectionString": {
+ "type": "string",
+ "description": "Gets or sets the connection string of the Postgre SQL database to connect to."
+ },
+ "HealthChecks": {
+ "type": "boolean",
+ "description": "Gets or sets a boolean value that indicates whether the database 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
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "type": "object"
+}
diff --git a/src/Components/Aspire.Oracle.ManagedDataAccess.Core/OracleManagedDataAccessCoreSettings.cs b/src/Components/Aspire.Oracle.ManagedDataAccess.Core/OracleManagedDataAccessCoreSettings.cs
new file mode 100644
index 00000000000..e0f45e415a9
--- /dev/null
+++ b/src/Components/Aspire.Oracle.ManagedDataAccess.Core/OracleManagedDataAccessCoreSettings.cs
@@ -0,0 +1,27 @@
+// 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.ManagedDataAccess.Core ;
+
+///
+/// Provides the client configuration settings for connecting to a PostgreSQL database using Npgsql.
+///
+public sealed class OracleManagedDataAccessCoreSettings
+{
+ ///
+ /// The connection string of the PostgreSQL database to connect to.
+ ///
+ public string? ConnectionString { get; set; }
+
+ ///
+ /// Gets or sets a boolean value that indicates whether the database health check is enabled or not.
+ /// Enabled by default.
+ ///
+ public bool HealthChecks { get; set; } = true;
+
+ ///
+ /// Gets or sets a boolean value that indicates whether the Open Telemetry tracing is enabled or not.
+ /// Enabled by default.
+ ///
+ public bool Tracing { get; set; } = true;
+}
diff --git a/src/Components/Aspire.Oracle.ManagedDataAccess.Core/README.md b/src/Components/Aspire.Oracle.ManagedDataAccess.Core/README.md
new file mode 100644
index 00000000000..c90fcdffcf0
--- /dev/null
+++ b/src/Components/Aspire.Oracle.ManagedDataAccess.Core/README.md
@@ -0,0 +1,111 @@
+# Aspire.Oracle.ManagedDataAccess.Core library
+
+Registers [OracleConnection](https://docs.oracle.com/en/database/oracle/oracle-database/23/odpnt/OracleConnectionClass.html) in the DI container for connecting Oracle® database. Enables corresponding health check and telemetry.
+
+## Getting started
+
+### Prerequisites
+
+- Oracle database and connection string for accessing the database.
+
+### Install the package
+
+Install the .NET Aspire Oracle Managed Data Access Core library with [NuGet](https://www.nuget.org):
+
+```dotnetcli
+dotnet add package Aspire.Oracle.ManagedDataAccess.Core
+```
+
+## Usage example
+
+In the _Program.cs_ file of your project, call the `AddOracleManagedDataAccessCore` extension method to register a `OracleConnection` for use via the dependency injection container. The method takes a connection name parameter.
+
+```csharp
+builder.AddOracleManagedDataAccessCore("oracledb");
+```
+
+You can then retrieve the `OracleConnection` instance using dependency injection. For example, to retrieve the connection from a Web API controller:
+
+```csharp
+private readonly OracleConnection _connection;
+
+public ProductsController(OracleConnection connection)
+{
+ _connection = connection;
+}
+```
+
+## Configuration
+
+The .NET Aspire Oracle Managed Data Access Core 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.AddOracleManagedDataAccessCore()`:
+
+```csharp
+builder.AddOracleManagedDataAccessCore("myConnection");
+```
+
+And then the connection string will be retrieved from the `ConnectionStrings` configuration section:
+
+```json
+{
+ "ConnectionStrings": {
+ "myConnection": "user id=system;password=password;data source=localhost:port/freepdb1"
+ }
+}
+```
+
+### Use configuration providers
+
+The .NET Aspire Oracle Managed Data Access Core component supports [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/api/microsoft.extensions.configuration). It loads the `OracleManagedDataAccessCoreSettings` from configuration by using the `Aspire:Oracle:ManagedDataAccess:Core` key. Example `appsettings.json` that configures some of the options:
+
+```json
+{
+ "Aspire": {
+ "Oracle": {
+ "ManagedDataAccess": {
+ "Core": {
+ "HealthChecks": true,
+ "Tracing": 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.AddOracleManagedDataAccessCore("oracledb", settings => settings.HealthChecks = false);
+```
+
+## AppHost extensions
+
+In your AppHost project, register a Oracle Database container and consume the connection using the following methods:
+
+```csharp
+var oracledb = builder.AddOracleDatabaseContainer("orcl").AddDatabase("freepdb1");
+
+var myService = builder.AddProject()
+ .WithReference(oracledb);
+```
+
+The `WithReference` method configures a connection in the `MyService` project named `oracledb`. In the _Program.cs_ file of `MyService`, the database connection can be consumed using:
+
+```csharp
+builder.AddOracleManagedDataAccessCore("oracledb");
+```
+
+## Additional documentation
+
+* https://github.com/oracle/dotnet-db-samples/tree/master
+* 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 427a1ac1c66..643561a789f 100644
--- a/src/Components/Aspire_Components_Progress.md
+++ b/src/Components/Aspire_Components_Progress.md
@@ -21,6 +21,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.ManagedDataAccess.Core | ✅ | | ✅ | ✅ | | ✅ | | ✅ |
Nomenclature used in the table above:
diff --git a/tests/Aspire.Oracle.ManagedDataAccess.Core.Tests/Aspire.Oracle.ManagedDataAccess.Core.Tests.csproj b/tests/Aspire.Oracle.ManagedDataAccess.Core.Tests/Aspire.Oracle.ManagedDataAccess.Core.Tests.csproj
new file mode 100644
index 00000000000..8ae8792523e
--- /dev/null
+++ b/tests/Aspire.Oracle.ManagedDataAccess.Core.Tests/Aspire.Oracle.ManagedDataAccess.Core.Tests.csproj
@@ -0,0 +1,12 @@
+
+
+
+ $(NetCurrent)
+
+
+
+
+
+
+
+
diff --git a/tests/Aspire.Oracle.ManagedDataAccess.Core.Tests/AspireOracleManagedDataAccessCoreExtensionsTests.cs b/tests/Aspire.Oracle.ManagedDataAccess.Core.Tests/AspireOracleManagedDataAccessCoreExtensionsTests.cs
new file mode 100644
index 00000000000..6abf5b8fee9
--- /dev/null
+++ b/tests/Aspire.Oracle.ManagedDataAccess.Core.Tests/AspireOracleManagedDataAccessCoreExtensionsTests.cs
@@ -0,0 +1,104 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Oracle.ManagedDataAccess.Client;
+using Xunit;
+
+namespace Aspire.Oracle.ManagedDataAccess.Core.Tests;
+
+public class AspireOracleManagedDataAccessCoreExtensionsTests
+{
+ private const string ConnectionString = "user id=system;password=password;data source=localhost:port/freepdb1";
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ReadsFromConnectionStringsCorrectly(bool useKeyed)
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair("ConnectionStrings:orcl", ConnectionString)
+ ]);
+
+ if (useKeyed)
+ {
+ builder.AddKeyedOracleManagedDataAccessCore("orcl");
+ }
+ else
+ {
+ builder.AddOracleManagedDataAccessCore("orcl");
+ }
+
+ var host = builder.Build();
+ var connection = useKeyed ?
+ host.Services.GetRequiredKeyedService("orcl") :
+ host.Services.GetRequiredService();
+
+ Assert.Equal(ConnectionString, connection.ConnectionString);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ConnectionStringCanBeSetInCode(bool useKeyed)
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair("ConnectionStrings:orcl", "unused")
+ ]);
+
+ static void SetConnectionString(OracleManagedDataAccessCoreSettings settings) => settings.ConnectionString = ConnectionString;
+ if (useKeyed)
+ {
+ builder.AddKeyedOracleManagedDataAccessCore("orcl", SetConnectionString);
+ }
+ else
+ {
+ builder.AddOracleManagedDataAccessCore("orcl", SetConnectionString);
+ }
+
+ var host = builder.Build();
+ var connection = useKeyed ?
+ host.Services.GetRequiredKeyedService("orcl") :
+ host.Services.GetRequiredService();
+
+ Assert.Equal(ConnectionString, connection.ConnectionString);
+ // the connection string from config should not be used since code set it explicitly
+ Assert.DoesNotContain("unused", connection.ConnectionString);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ConnectionNameWinsOverConfigSection(bool useKeyed)
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ var key = useKeyed ? "orcl" : null;
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair(ConformanceTests.CreateConfigKey("Aspire:Oracle:ManagedDataAccess:Core", key, "ConnectionString"), "unused"),
+ new KeyValuePair("ConnectionStrings:orcl", ConnectionString)
+ ]);
+
+ if (useKeyed)
+ {
+ builder.AddKeyedOracleManagedDataAccessCore("orcl");
+ }
+ else
+ {
+ builder.AddOracleManagedDataAccessCore("orcl");
+ }
+
+ var host = builder.Build();
+ var connection = useKeyed ?
+ host.Services.GetRequiredKeyedService("orcl") :
+ host.Services.GetRequiredService();
+
+ Assert.Equal(ConnectionString, connection.ConnectionString);
+ // the connection string from config should not be used since it was found in ConnectionStrings
+ Assert.DoesNotContain("unused", connection.ConnectionString);
+ }
+}
diff --git a/tests/Aspire.Oracle.ManagedDataAccess.Core.Tests/ConfigurationTests.cs b/tests/Aspire.Oracle.ManagedDataAccess.Core.Tests/ConfigurationTests.cs
new file mode 100644
index 00000000000..9c05a3a8701
--- /dev/null
+++ b/tests/Aspire.Oracle.ManagedDataAccess.Core.Tests/ConfigurationTests.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 Xunit;
+
+namespace Aspire.Oracle.ManagedDataAccess.Core.Tests;
+
+public class ConfigurationTests
+{
+ [Fact]
+ public void ConnectionStringIsNullByDefault()
+ => Assert.Null(new OracleManagedDataAccessCoreSettings().ConnectionString);
+
+ [Fact]
+ public void HealthCheckIsEnabledByDefault()
+ => Assert.True(new OracleManagedDataAccessCoreSettings().HealthChecks);
+
+ [Fact]
+ public void TracingIsEnabledByDefault()
+ => Assert.True(new OracleManagedDataAccessCoreSettings().Tracing);
+}
diff --git a/tests/Aspire.Oracle.ManagedDataAccess.Core.Tests/ConformanceTests.cs b/tests/Aspire.Oracle.ManagedDataAccess.Core.Tests/ConformanceTests.cs
new file mode 100644
index 00000000000..ad66354cdb7
--- /dev/null
+++ b/tests/Aspire.Oracle.ManagedDataAccess.Core.Tests/ConformanceTests.cs
@@ -0,0 +1,139 @@
+// 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.ConformanceTests;
+using Microsoft.DotNet.RemoteExecutor;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Oracle.ManagedDataAccess.Client;
+using Xunit;
+
+namespace Aspire.Oracle.ManagedDataAccess.Core.Tests;
+
+public class ConformanceTests : ConformanceTests
+{
+ private const string ConnectionSting = "user id=system;password=password;data source=localhost:port/freepdb1";
+
+ private static readonly Lazy s_canConnectToServer = new(GetCanConnect);
+
+ protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Scoped;
+
+ protected override string ActivitySourceName => "Oracle.ManagedDataAccess.Core";
+
+ protected override string[] RequiredLogCategories => [];
+
+ protected override bool SupportsKeyedRegistrations => true;
+
+ protected override bool CanConnectToServer => s_canConnectToServer.Value;
+
+ protected override string JsonSchemaPath => "src/Components/Aspire.Oracle.ManagedDataAccess.Core/ConfigurationSchema.json";
+
+ protected override string ValidJsonConfig => """
+ {
+ "Aspire": {
+ "Oracle": {
+ "ManagedDataAccess":{
+ "Core": {
+ "ConnectionString": "YOUR_CONNECTION_STRING",
+ "HealthChecks": false,
+ "Tracing": true
+ }
+ }
+ }
+ }
+ }
+ """;
+
+ protected override (string json, string error)[] InvalidJsonToErrorMessage => new[]
+ {
+ ("""{"Aspire": { "Oracle": { "ManagedDataAccess": { "Core": { "Tracing": 0 }}}}}""", "Value is \"integer\" but should be \"boolean\""),
+ ("""{"Aspire": { "Oracle": { "ManagedDataAccess": { "Core": { "ConnectionString": "Con", "HealthChecks": "false"}}}}}""", "Value is \"string\" but should be \"boolean\"")
+ };
+
+ protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null)
+ => configuration.AddInMemoryCollection(new KeyValuePair[1]
+ {
+ new KeyValuePair(CreateConfigKey("Aspire:Oracle:ManagedDataAccess:Core", key, "ConnectionString"), ConnectionSting)
+ });
+
+ protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null)
+ {
+ if (key is null)
+ {
+ builder.AddOracleManagedDataAccessCore("orcl", configure);
+ }
+ else
+ {
+ builder.AddKeyedOracleManagedDataAccessCore(key, configure);
+ }
+ }
+
+ protected override void SetHealthCheck(OracleManagedDataAccessCoreSettings options, bool enabled)
+ => options.HealthChecks = enabled;
+
+ protected override void SetTracing(OracleManagedDataAccessCoreSettings options, bool enabled)
+ => options.Tracing = enabled;
+
+ protected override void SetMetrics(OracleManagedDataAccessCoreSettings options, bool enabled) {}
+
+ protected override void TriggerActivity(OracleConnection connection)
+ {
+ connection.Open();
+ using OracleCommand command = connection.CreateCommand();
+ command.CommandText = "SELECT 1 FROM DUAL";
+ command.ExecuteScalar();
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("key")]
+ public void ConnectionCanBeResolved(string? key)
+ {
+ using IHost host = CreateHostWithComponent(key: key);
+
+ OracleConnection? oracleConnection = Resolve();
+
+ Assert.NotNull(oracleConnection);
+
+ T? Resolve() => key is null ? host.Services.GetService() : host.Services.GetKeyedService(key);
+ }
+
+ [ConditionalFact]
+ public void TracingEnablesTheRightActivitySource()
+ {
+ SkipIfCanNotConnectToServer();
+
+ RemoteExecutor.Invoke(() => ActivitySourceTest(key: null)).Dispose();
+ }
+
+ [ConditionalFact]
+ public void TracingEnablesTheRightActivitySource_Keyed()
+ {
+ SkipIfCanNotConnectToServer();
+
+ RemoteExecutor.Invoke(() => ActivitySourceTest(key: "key")).Dispose();
+ }
+
+ private static bool GetCanConnect()
+ {
+ OracleConnection connection = new(ConnectionSting);
+ OracleCommand? cmd = null;
+
+ try
+ {
+ connection.Open();
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ finally
+ {
+ cmd?.Dispose();
+ connection.Dispose();
+ }
+
+ return true;
+ }
+}