Skip to content

Commit

Permalink
Adds Aspire Oracle EntityFrameworkCore Database component. (#1295)
Browse files Browse the repository at this point in the history
* Adds Aspire Oracle EntityFrameworkCore Database component.

* AppModel apis

* tests for oracle hosting

* adjusting to reviews

* Oracle has a 30 character password limit

* functional test

* adjusting to reviews

* configurationschema was outdated

* adjusting to reviews

* updated with the latest bits
  • Loading branch information
andrevlins authored Jan 3, 2024
1 parent 9d4bd62 commit 398dd70
Show file tree
Hide file tree
Showing 29 changed files with 1,311 additions and 2 deletions.
14 changes: 14 additions & 0 deletions Aspire.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
<PackageVersion Include="MySqlConnector.DependencyInjection" Version="2.3.1" />
<PackageVersion Include="Npgsql.DependencyInjection" Version="8.0.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageVersion Include="Oracle.EntityFrameworkCore" Version="8.21.121" />
<PackageVersion Include="Polly" Version="8.2.0" />
<PackageVersion Include="RabbitMQ.Client" Version="6.7.0" />
<PackageVersion Include="StackExchange.Redis" Version="2.7.4" />
Expand All @@ -105,4 +106,4 @@
<PackageVersion Include="Microsoft.Signed.Wix" Version="1.0.0-v3.14.0.5722" />
<PackageVersion Include="Microsoft.DotNet.Build.Tasks.Installers" Version="8.0.0-beta.23564.4" />
</ItemGroup>
</Project>
</Project>
11 changes: 11 additions & 0 deletions src/Aspire.Hosting/Oracle/IOracleDatabaseParentResource.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Represents a Oracle Database resource that requires a connection string.
/// </summary>
public interface IOracleDatabaseParentResource : IResourceWithConnectionString, IResourceWithEnvironment
{
}
105 changes: 105 additions & 0 deletions src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Provides extension methods for adding Oracle Database resources to an <see cref="IDistributedApplicationBuilder"/>.
/// </summary>
public static class OracleDatabaseBuilderExtensions
{
private const string PasswordEnvVarName = "ORACLE_PWD";

/// <summary>
/// Adds a Oracle Database container to the application model. The default image is "database/free" and the tag is "latest".
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
/// <param name="port">The host port for Oracle Database.</param>
/// <param name="password">The password for the Oracle Database container. Defaults to a random password.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{OracleDatabaseContainerResource}"/>.</returns>
public static IResourceBuilder<OracleDatabaseContainerResource> 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);
}
});
}

/// <summary>
/// Adds a Oracle Database resource to the application model. A container is used for local development.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{OracleDatabaseServerResource}"/>.</returns>
public static IResourceBuilder<OracleDatabaseServerResource> 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);
}

/// <summary>
/// Adds a Oracle Database database to the application model.
/// </summary>
/// <param name="builder">The Oracle Database server resource builder.</param>
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{OracleDatabaseResource}"/>.</returns>
public static IResourceBuilder<OracleDatabaseResource> AddDatabase(this IResourceBuilder<IOracleDatabaseParentResource> 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(); // }
}
}
33 changes: 33 additions & 0 deletions src/Aspire.Hosting/Oracle/OracleDatabaseContainerResource.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// A resource that represents a Oracle Database container.
/// </summary>
/// <param name="name">The name of the resource.</param>
/// <param name="password">The Oracle Database server password.</param>
public class OracleDatabaseContainerResource(string name, string password) : ContainerResource(name), IOracleDatabaseParentResource
{
public string Password { get; } = password;

/// <summary>
/// Gets the connection string for the Oracle Database server.
/// </summary>
/// <returns>A connection string for the Oracle Database server in the form "user id=system;password=password;data source=localhost:port".</returns>
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;
}
}
30 changes: 30 additions & 0 deletions src/Aspire.Hosting/Oracle/OracleDatabaseResource.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// A resource that represents a Oracle Database database. This is a child resource of a <see cref="OracleDatabaseContainerResource"/>.
/// </summary>
/// <param name="name">The name of the resource.</param>
/// <param name="oracleParentResource">The Oracle Database parent resource associated with this database.</param>
public class OracleDatabaseResource(string name, IOracleDatabaseParentResource oracleParentResource) : Resource(name), IResourceWithParent<IOracleDatabaseParentResource>, IResourceWithConnectionString
{
public IOracleDatabaseParentResource Parent { get; } = oracleParentResource;

/// <summary>
/// Gets the connection string for the Oracle Database.
/// </summary>
/// <returns>A connection string for the Oracle Database.</returns>
public string? GetConnectionString()
{
if (Parent.GetConnectionString() is { } connectionString)
{
return $"{connectionString}/{Name}";
}
else
{
throw new DistributedApplicationException("Parent resource connection string was null.");
}
}
}
33 changes: 33 additions & 0 deletions src/Aspire.Hosting/Oracle/OracleDatabaseServerResource.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// A resource that represents a Oracle Database container.
/// </summary>
/// <param name="name">The name of the resource.</param>
/// <param name="password">The Oracle Database server password.</param>
public class OracleDatabaseServerResource(string name, string password) : Resource(name), IOracleDatabaseParentResource
{
public string Password { get; } = password;

/// <summary>
/// Gets the connection string for the Oracle Database server.
/// </summary>
/// <returns>A connection string for the Oracle Database server in the form "user id=system;password=password;data source=host:port".</returns>
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

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

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

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

</Project>
Loading

0 comments on commit 398dd70

Please sign in to comment.