Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/ravendb/RavenDB.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

var serverSettings = RavenDBServerSettings.Unsecured();
var ravendb = builder.AddRavenDB("ravendb", serverSettings);
ravendb.AddDatabase("ravenDatabase");
ravendb.AddDatabase("ravenDatabase", ensureCreated: true);

builder.AddProject<CommunityToolkit_Aspire_Hosting_RavenDB_ApiService>("apiservice")
.WithReference(ravendb)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Data.Common;
using System.Security.Cryptography.X509Certificates;

namespace CommunityToolkit.Aspire.Hosting.RavenDB;

Expand All @@ -15,6 +16,7 @@ internal static class HealthChecksExtensions
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="connectionStringFactory">A factory to build the connection string to use.</param>
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'ravendb' will be used for the name.</param>
/// <param name="certificate">The client certificate to use for the connection. Optional.</param>
/// <param name="failureStatus">that should be reported when the health check fails. Optional. If <c>null</c> then
/// the default status of <see cref="HealthStatus.Unhealthy"/> will be reported.</param>
/// <param name="tags">A list of tags that can be used to filter sets of health checks. Optional.</param>
Expand All @@ -24,6 +26,7 @@ public static IHealthChecksBuilder AddRavenDB(
this IHealthChecksBuilder builder,
Func<IServiceProvider, string> connectionStringFactory,
string? name = default,
X509Certificate2? certificate = null,
HealthStatus? failureStatus = default,
IEnumerable<string>? tags = default,
TimeSpan? timeout = default)
Expand All @@ -33,7 +36,7 @@ public static IHealthChecksBuilder AddRavenDB(
sp =>
{
var connectionString = ValidateConnectionString(connectionStringFactory, sp);
return new RavenDBHealthCheck(new RavenDBOptions { Urls = new[] { connectionString } });
return new RavenDBHealthCheck(new RavenDBOptions { Urls = new[] { connectionString }, Certificate = certificate});
},
failureStatus,
tags,
Expand All @@ -47,6 +50,7 @@ public static IHealthChecksBuilder AddRavenDB(
/// <param name="connectionStringFactory">A factory to build the connection string to use.</param>
/// <param name="databaseName">The database name to check.</param>
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'ravendb' will be used for the name.</param>
/// <param name="certificate">The client certificate to use for the connection. Optional.</param>
/// <param name="failureStatus">that should be reported when the health check fails. Optional. If <c>null</c> then
/// the default status of <see cref="HealthStatus.Unhealthy"/> will be reported.</param>
/// <param name="tags">A list of tags that can be used to filter sets of health checks. Optional.</param>
Expand All @@ -57,6 +61,7 @@ public static IHealthChecksBuilder AddRavenDB(
Func<IServiceProvider, string> connectionStringFactory,
string databaseName,
string? name = default,
X509Certificate2? certificate = null,
HealthStatus? failureStatus = default,
IEnumerable<string>? tags = default,
TimeSpan? timeout = default)
Expand All @@ -69,7 +74,8 @@ public static IHealthChecksBuilder AddRavenDB(
return new RavenDBHealthCheck(new RavenDBOptions
{
Urls = new[] { connectionString },
Database = databaseName
Database = databaseName,
Certificate = certificate
});
},
failureStatus,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
using Aspire.Hosting.ApplicationModel;
using CommunityToolkit.Aspire.Hosting.RavenDB;
using Microsoft.Extensions.DependencyInjection;
using Raven.Client.Documents;
using Raven.Client.ServerWide;
using Raven.Client.ServerWide.Operations;
using System.Data.Common;

namespace Aspire.Hosting;

Expand Down Expand Up @@ -53,7 +57,15 @@ public static IResourceBuilder<RavenDBServerResource> AddRavenDB(this IDistribut
ArgumentNullException.ThrowIfNull(builder);

var environmentVariables = GetEnvironmentVariablesFromServerSettings(serverSettings);
return builder.AddRavenDB(name, secured: serverSettings is RavenDBSecuredServerSettings, environmentVariables);
var securedSettings = serverSettings as RavenDBSecuredServerSettings;

var serverResource = new RavenDBServerResource(name, isSecured: securedSettings is not null)
{
PublicServerUrl = securedSettings?.PublicServerUrl,
ClientCertificate = securedSettings?.ClientCertificate
};

return AddRavenDbInternal(builder, name, serverResource, environmentVariables, serverSettings.Port, serverSettings.TcpPort);
}

/// <summary>
Expand All @@ -79,24 +91,48 @@ public static IResourceBuilder<RavenDBServerResource> AddRavenDB(this IDistribut

var serverResource = new RavenDBServerResource(name, secured);

return AddRavenDbInternal(builder, name, serverResource, environmentVariables, port, tcpPort: null);
}

private static IResourceBuilder<RavenDBServerResource> AddRavenDbInternal(
IDistributedApplicationBuilder builder,
string name,
RavenDBServerResource serverResource,
Dictionary<string, object> environmentVariables,
int? port,
int? tcpPort)
{
string? connectionString = null;
builder.Eventing.Subscribe<ConnectionStringAvailableEvent>(serverResource, async (@event, ct) =>
builder.Eventing.Subscribe<ConnectionStringAvailableEvent>(serverResource, async (_, ct) =>
{
connectionString = await serverResource.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);
connectionString = await serverResource.ConnectionStringExpression.GetValueAsync(ct)
.ConfigureAwait(false);

if (connectionString is null)
throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{serverResource.Name}' resource but the connection string was null.");
throw new DistributedApplicationException(
$"ConnectionStringAvailableEvent was published for the '{serverResource.Name}' resource but the connection string was null.");
});

var healthCheckKey = $"{name}_check";
builder.Services.AddHealthChecks()
.AddRavenDB(sp => connectionString ?? throw new InvalidOperationException("Connection string is unavailable"),
name: healthCheckKey);

return builder
.AddResource(serverResource)
.WithEndpoint(port: port, targetPort: secured ? 443 : 8080, scheme: serverResource.PrimaryEndpointName, name: serverResource.PrimaryEndpointName, isProxied: false)
.WithEndpoint(port: 38888, name: serverResource.TcpEndpointName, isProxied: false)
.AddRavenDB(_ => connectionString ?? throw new InvalidOperationException("Connection string is unavailable"),
name: healthCheckKey,
certificate: serverResource.ClientCertificate);

var effectiveTcpPort = tcpPort ?? 38888;

return builder.AddResource(serverResource)
.WithEndpoint(
port: port,
targetPort: serverResource.IsSecured ? 443 : 8080,
scheme: serverResource.PrimaryEndpointName,
name: serverResource.PrimaryEndpointName,
isProxied: false)
.WithEndpoint(
port: effectiveTcpPort,
targetPort: effectiveTcpPort,
name: serverResource.TcpEndpointName,
isProxied: false)
.WithImage(RavenDBContainerImageTags.Image, RavenDBContainerImageTags.Tag)
.WithImageRegistry(RavenDBContainerImageTags.Registry)
.WithEnvironment(context => ConfigureEnvironmentVariables(context, serverResource, environmentVariables))
Expand Down Expand Up @@ -126,6 +162,9 @@ private static Dictionary<string, object> GetEnvironmentVariablesFromServerSetti

if (securedServerSettings.CertificatePassword is not null)
environmentVariables.TryAdd("RAVEN_Security_Certificate_Password", securedServerSettings.CertificatePassword);

var publicUri = new Uri(securedServerSettings.PublicServerUrl);
environmentVariables.TryAdd("RAVEN_PublicServerUrl_Tcp", $"tcp://{publicUri.Host}:{serverSettings.TcpPort ?? 38888}");
}

return environmentVariables;
Expand All @@ -134,17 +173,19 @@ private static Dictionary<string, object> GetEnvironmentVariablesFromServerSetti
private static void ConfigureEnvironmentVariables(EnvironmentCallbackContext context, RavenDBServerResource serverResource, Dictionary<string, object>? environmentVariables = null)
{
context.EnvironmentVariables.TryAdd("RAVEN_ServerUrl_Tcp", $"{serverResource.TcpEndpoint.Scheme}://0.0.0.0:{serverResource.TcpEndpoint.Port}");
context.EnvironmentVariables.TryAdd("RAVEN_PublicServerUrl_Tcp", serverResource.TcpEndpoint.Url);

if (environmentVariables is null)
{
context.EnvironmentVariables.TryAdd("RAVEN_Setup_Mode", "None");
context.EnvironmentVariables.TryAdd("RAVEN_Security_UnsecuredAccessAllowed", "PrivateNetwork");
return;
}
else
{
foreach (var environmentVariable in environmentVariables)
context.EnvironmentVariables.TryAdd(environmentVariable.Key, environmentVariable.Value);
}

foreach (var environmentVariable in environmentVariables)
context.EnvironmentVariables.TryAdd(environmentVariable.Key, environmentVariable.Value);
context.EnvironmentVariables.TryAdd("RAVEN_PublicServerUrl_Tcp", serverResource.TcpEndpoint.Url);
}

/// <summary>
Expand All @@ -153,12 +194,14 @@ private static void ConfigureEnvironmentVariables(EnvironmentCallbackContext con
/// <param name="builder">The resource builder for the RavenDB server.</param>
/// <param name="name">The name of the database resource.</param>
/// <param name="databaseName">The name of the database to create/add. Defaults to the same name as the resource if not provided.</param>
/// <param name="ensureCreated">Indicates whether the database should be created on startup if it does not already exist.</param>
/// <returns>A resource builder for the newly added RavenDB database resource.</returns>
/// <exception cref="DistributedApplicationException">Thrown when the connection string cannot be retrieved during configuration.</exception>
/// <exception cref="InvalidOperationException">Thrown when the connection string is unavailable.</exception>
public static IResourceBuilder<RavenDBDatabaseResource> AddDatabase(this IResourceBuilder<RavenDBServerResource> builder,
[ResourceName] string name,
string? databaseName = null)
string? databaseName = null,
bool ensureCreated = false)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(name);
Expand All @@ -183,10 +226,40 @@ public static IResourceBuilder<RavenDBDatabaseResource> AddDatabase(this IResour
builder.ApplicationBuilder.Services.AddHealthChecks()
.AddRavenDB(sp => connectionString ?? throw new InvalidOperationException("Connection string is unavailable"),
databaseName: databaseName,
name: healthCheckKey);
name: healthCheckKey,
certificate: databaseResource.Parent.ClientCertificate);

var dbBuilder = builder.ApplicationBuilder.AddResource(databaseResource);

if (ensureCreated)
{
dbBuilder.OnResourceReady(async (resource, _, ct) =>
{
var connString = await databaseResource.ConnectionStringExpression.GetValueAsync(ct);
if (string.IsNullOrEmpty(connString))
throw new InvalidOperationException("RavenDB connection string is not available.");

var csb = new DbConnectionStringBuilder { ConnectionString = connString };

if (!csb.TryGetValue("URL", out var urlObj) || urlObj is not string url)
throw new InvalidOperationException("Connection string is missing 'URL'.");

using var store = new DocumentStore
{
Urls = [url],
Certificate = databaseResource.Parent.ClientCertificate
}.Initialize();

var record = await store.Maintenance.Server
.SendAsync(new GetDatabaseRecordOperation(resource.DatabaseName), ct);

if (record == null)
await store.Maintenance.Server.SendAsync(new CreateDatabaseOperation(new DatabaseRecord(resource.DatabaseName)), ct);

});
}

return builder.ApplicationBuilder
.AddResource(databaseResource);
return dbBuilder;
}

/// <summary>
Expand Down Expand Up @@ -217,4 +290,42 @@ public static IResourceBuilder<RavenDBServerResource> WithDataVolume(this IResou

return builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "data"), "/var/lib/ravendb/data", isReadOnly);
}

/// <summary>
/// Adds a bind mount for the logs folder to a RavenDB container resource.
/// </summary>
/// <param name="builder">The resource builder for the RavenDB server.</param>
/// <param name="source">The source directory on the host to mount into the container.</param>
/// <param name="isReadOnly">Indicates whether the bind mount should be read-only. Defaults to false.</param>
/// <returns>The <see cref="IResourceBuilder{T}"/> for the RavenDB server resource.</returns>
public static IResourceBuilder<RavenDBServerResource> WithLogBindMount(
this IResourceBuilder<RavenDBServerResource> builder,
string source,
bool isReadOnly = false)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(source);

return builder.WithBindMount(source, "/var/log/ravendb/logs", isReadOnly);
}

/// <summary>
/// Adds a named volume for the logs folder to a RavenDB container resource.
/// </summary>
/// <param name="builder">The resource builder for the RavenDB server.</param>
/// <param name="name">
/// Optional name for the volume. Defaults to a generated name if not provided.
/// </param>
/// <param name="isReadOnly">Indicates whether the volume should be read-only. Defaults to false.</param>
/// <returns>The <see cref="IResourceBuilder{T}"/> for the RavenDB server resource.</returns>
public static IResourceBuilder<RavenDBServerResource> WithLogVolume(
this IResourceBuilder<RavenDBServerResource> builder,
string? name = null,
bool isReadOnly = false)
{
ArgumentNullException.ThrowIfNull(builder);

return builder.WithVolume(name ?? VolumeNameGenerator.Generate(builder, "logs"), "/var/log/ravendb/logs", isReadOnly);
}

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Aspire.Hosting.ApplicationModel;
using System.Security.Cryptography.X509Certificates;

namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// A resource that represents a RavenDB container.
Expand All @@ -8,7 +10,7 @@ public class RavenDBServerResource(string name, bool isSecured) : ContainerResou
/// <summary>
/// Indicates whether the server connection is secured (HTTPS) or not (HTTP).
/// </summary>
private bool IsSecured { get; } = isSecured;
internal bool IsSecured { get; } = isSecured;

/// <summary>
/// Gets the protocol used for the primary endpoint, based on the security setting ("http" or "https").
Expand All @@ -19,6 +21,17 @@ public class RavenDBServerResource(string name, bool isSecured) : ContainerResou
/// </summary>
internal string TcpEndpointName = "tcp";

/// <summary>
/// The public server URL (domain) configured for this resource./>.
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML doc comment has a malformed closing tag. The closing summary tag should be </summary> instead of />.

Suggested change
/// The public server URL (domain) configured for this resource./>.
/// The public server URL (domain) configured for this resource.

Copilot uses AI. Check for mistakes.
/// </summary>
internal string? PublicServerUrl { get; init; }

/// <summary>
/// Optional client certificate used by hosting code (health checks, database
/// creation, etc.) when connecting to this RavenDB server in secured setups.
/// </summary>
internal X509Certificate2? ClientCertificate { get; init; }

private EndpointReference? _primaryEndpoint;
private EndpointReference? tcpEndpoint;

Expand Down Expand Up @@ -46,8 +59,17 @@ public class RavenDBServerResource(string name, bool isSecured) : ContainerResou
/// Gets the connection string expression for the RavenDB server,
/// formatted as "http(s)://{Host}:{Port}" depending on the security setting.
/// </summary>
public ReferenceExpression ConnectionStringExpression => ReferenceExpression.Create(
$"URL={(IsSecured ? "https://" : "http://")}{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}");
public ReferenceExpression ConnectionStringExpression
{
get
{
if (IsSecured && !string.IsNullOrEmpty(PublicServerUrl))
return ReferenceExpression.Create($"URL={PublicServerUrl}");

return ReferenceExpression.Create(
$"URL={(IsSecured ? "https://" : "http://")}{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}");
}
}

/// <summary>
/// Gets the connection URI expression for the RavenDB server.
Expand Down
Loading