File: ReplicaSet\MongoDBReplicaSetResource.cs
Web Access
Project: src\src\Aspire.Hosting.MongoDB\Aspire.Hosting.MongoDB.csproj (Aspire.Hosting.MongoDB)
// 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;
 
#pragma warning disable ASPIREMONGODB001
 
namespace Aspire.Hosting.ApplicationModel;
 
/// <summary>
/// Represents a MongoDB replica set resource in the application model.
/// A replica set is a group of MongoDB servers that maintain the same data set, providing redundancy and high availability.
/// </summary>
/// <remarks>
/// This resource is a logical grouping of multiple <see cref="MongoDBServerResource"/> instances that are configured as members of the same replica set.
/// </remarks>
/// <param name="name">The name of the resource, which is also the name of the replica set itself.</param>
/// <param name="keyFile">The content of the keyfile the members use to authenticate to each other. Required.</param>
/// <param name="sharedUserName">The user name every member authenticates with, or <see langword="null"/> to use the default user name.</param>
/// <param name="sharedPassword">The password every member authenticates with. Required.</param>
[Experimental("ASPIREMONGODB001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
[AspireExport(ExposeProperties = true)]
public sealed class MongoDBReplicaSetResource(
    string name,
    ParameterResource keyFile,
    ParameterResource? sharedUserName,
    ParameterResource sharedPassword
) : Resource(name), IResourceWithWaitSupport, IResourceWithConnectionString
{
    /// <summary>
    /// Gets the combined connection string for the MongoDB replica set, which includes the endpoints of all members, interpretable by the MongoDB driver.
    /// </summary>
    public ReferenceExpression ConnectionStringExpression => BuildConnectionString();
 
    /// <summary>
    /// Gets the parameter that contains the content of the key file used for internal authentication between members of the MongoDB replica set.
    /// </summary>
    public ParameterResource SharedKeyFileParameter { get; } = keyFile ?? throw new ArgumentNullException(nameof(keyFile));
 
    /// <summary>
    /// Gets the parameter that contains the username for authenticating to the MongoDB replica set.
    /// </summary>
    /// <remarks>
    /// This will be the same across all members of the replica set, and is used in conjunction with <see cref="SharedPasswordParameter"/> for authentication.
    /// </remarks>
    public ParameterResource? SharedUserNameParameter { get; } = sharedUserName;
 
    /// <summary>
    /// Gets the parameter that contains the password for authenticating to the MongoDB replica set.
    /// </summary>
    /// <remarks>
    /// This will be the same across all members of the replica set, and is used in conjunction with <see cref="SharedUserNameParameter"/> for authentication.
    /// </remarks>
    public ParameterResource SharedPasswordParameter { get; } = sharedPassword ?? throw new ArgumentNullException(nameof(sharedPassword));
 
    /// <summary>
    /// Gets a reference to the username for the MongoDB replica set.
    /// </summary>
    public ReferenceExpression SharedUserNameReference =>
        SharedUserNameParameter is null
            ? ReferenceExpression.Create($"{MongoDBServerResource.DefaultUserName}")
            : ReferenceExpression.Create($"{SharedUserNameParameter}");
 
    /// <summary>
    /// The members that are currently stopped. The replica set has no process of its own for the orchestrator to follow,
    /// so its state is derived from the state of its members.
    /// </summary>
    internal HashSet<string> StoppedMembers { get; } = new(StringComparers.ResourceName);
 
    /// <summary>
    /// Whether the replica set has been configured, meaning it can be reported as running again if its members come back.
    /// </summary>
    internal bool IsConfigured { get; set; }
 
    /// <summary>
    /// Gets the MongoDB server resources that are members of this replica set, in the order they were added.
    /// </summary>
    public IEnumerable<MongoDBServerResource> Members => Annotations.OfType<MongoReplicaSetMemberAnnotation>().Select(a => a.Member);
 
    private ReferenceExpression BuildConnectionString()
    {
        var membersList = Members.ToList();
        if (membersList is [])
        {
            throw new InvalidOperationException($"Cannot build connection string for MongoDB replica set resource '{Name}' because it does not have any members.");
        }
 
        var builder = new ReferenceExpressionBuilder();
 
        // Build the seed list `mongodb://host1:port1,host2:port2,.../?replicaSet=<name>` — see https://www.mongodb.com/docs/manual/reference/connection-string/#dns-seedlist-connection-format
        builder.AppendLiteral("mongodb://");
 
        if (SharedUserNameParameter is not null)
        {
            builder.Append($"{SharedUserNameParameter:uri}:{SharedPasswordParameter:uri}@");
        }
        else
        {
            builder.Append($"{MongoDBServerResource.DefaultUserName:uri}:{SharedPasswordParameter:uri}@");
        }
 
        for (var i = 0; i < membersList.Count; i++)
        {
            var member = membersList[i];
            builder.Append($"{member.PrimaryEndpoint.Property(EndpointProperty.HostAndPort)}");
            if (i < membersList.Count - 1)
            {
                builder.AppendLiteral(",");
            }
        }
 
        builder.AppendLiteral($"/?replicaSet={Name}");
 
        builder.AppendLiteral("&authSource=");
        builder.Append($"{MongoDBServerResource.DefaultAuthenticationDatabase:uri}");
        builder.AppendLiteral("&authMechanism=");
        builder.Append($"{MongoDBServerResource.DefaultAuthenticationMechanism:uri}");
 
        // NOTE: TLS is turned on lazily (at `BeforeStartEvent` time, once a certificate is known to be available), so the
        // flag has to be resolved lazily here too. All members of a replica set share the same TLS configuration, so the
        // first member's endpoint is representative of the set.
        builder.Append($"{membersList[0].PrimaryEndpoint.GetTlsValue(
            enabledValue: ReferenceExpression.Create($"&tls=true"),
            disabledValue: ReferenceExpression.Empty)}");
 
        return builder.Build();
    }
 
    IEnumerable<KeyValuePair<string, ReferenceExpression>> IResourceWithConnectionString.GetConnectionProperties()
    {
        // NOTE: Unlike a single MongoDB server, a replica set has no one host and port to expose: clients are expected to
        // discover the members through the seed list in the connection string, so only the credentials, the replica set
        // name and the full URI are exposed here.
        yield return new("Username", SharedUserNameReference);
        yield return new("Password", ReferenceExpression.Create($"{SharedPasswordParameter}"));
        yield return new("AuthenticationDatabase", ReferenceExpression.Create($"{MongoDBServerResource.DefaultAuthenticationDatabase}"));
        yield return new("AuthenticationMechanism", ReferenceExpression.Create($"{MongoDBServerResource.DefaultAuthenticationMechanism}"));
        yield return new("ReplicaSetName", ReferenceExpression.Create($"{Name}"));
        yield return new("Uri", ConnectionStringExpression);
    }
}