| File: Dcp\PersistentPortAllocationStore.cs | Web Access |
| Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Text.Json.Nodes; using Aspire.Hosting.ApplicationModel; using Microsoft.Extensions.Logging; namespace Aspire.Hosting.Dcp; internal sealed class PersistentPortAllocationStore( AppHostLocalStateStore stateStore, ILogger<PersistentPortAllocationStore> logger) { private const string StateKey = "portAllocations"; private const int CurrentVersion = 1; public bool IsAvailable => stateStore.IsAvailable; public IReadOnlyDictionary<PersistentPortAllocationKey, int> GetAllocations() { return stateStore.Read( StateKey, value => TryReadPortAllocations(value, out var allocations) ? allocations : [], new Dictionary<PersistentPortAllocationKey, int>()); } public void SetPort(PersistentPortAllocationKey key, int port) { stateStore.Update(StateKey, value => { if (!TryReadPortAllocations(value, out var allocations)) { return value; } if (allocations.TryGetValue(key, out var existingPort) && existingPort == port) { return value; } allocations[key] = port; return WritePortAllocations(allocations); }); } private bool TryReadPortAllocations( JsonNode? value, out Dictionary<PersistentPortAllocationKey, int> allocations) { allocations = []; if (value is null) { return true; } if (value is not JsonObject state) { logger.LogWarning("Ignoring invalid {StateKey} AppHost local state value.", StateKey); return true; } if (state["version"] is not JsonValue versionValue || !versionValue.TryGetValue<int>(out var version)) { logger.LogWarning("Ignoring invalid {StateKey} AppHost local state value.", StateKey); return true; } if (version != CurrentVersion) { logger.LogWarning( "Ignoring unsupported {StateKey} AppHost local state value with version {Version}.", StateKey, version); return false; } if (state["resources"] is not JsonObject resourcesObject) { return true; } foreach (var resource in resourcesObject) { if (resource.Value is not JsonObject endpointsObject) { continue; } foreach (var endpoint in endpointsObject) { if (endpoint.Value is not JsonObject endpointObject) { continue; } if (TryGetPort(endpointObject["port"], out var publicPort)) { allocations[new PersistentPortAllocationKey(resource.Key, endpoint.Key)] = publicPort; } } } return true; static bool TryGetPort(JsonNode? node, out int port) { if (node is JsonValue value && value.TryGetValue(out port) && port is > 0 and <= 65535) { return true; } port = default; return false; } } private static JsonObject WritePortAllocations(Dictionary<PersistentPortAllocationKey, int> allocations) { var resourcesObject = new JsonObject(); foreach (var allocation in allocations .OrderBy(static allocation => allocation.Key.ResourceName, StringComparer.Ordinal) .ThenBy(static allocation => allocation.Key.EndpointName, StringComparer.Ordinal)) { if (resourcesObject[allocation.Key.ResourceName] is not JsonObject endpointsObject) { endpointsObject = new JsonObject(); resourcesObject[allocation.Key.ResourceName] = endpointsObject; } if (endpointsObject[allocation.Key.EndpointName] is not JsonObject endpointObject) { endpointObject = new JsonObject(); endpointsObject[allocation.Key.EndpointName] = endpointObject; } endpointObject["port"] = allocation.Value; } return new JsonObject { ["version"] = CurrentVersion, ["resources"] = resourcesObject }; } } internal readonly record struct PersistentPortAllocationKey(string ResourceName, string EndpointName) { public bool Equals(PersistentPortAllocationKey other) => StringComparers.ResourceName.Equals(ResourceName, other.ResourceName) && StringComparers.EndpointAnnotationName.Equals(EndpointName, other.EndpointName); public override int GetHashCode() { var hash = new HashCode(); hash.Add(ResourceName, StringComparers.ResourceName); hash.Add(EndpointName, StringComparers.EndpointAnnotationName); return hash.ToHashCode(); } }