| File: NativeProviderDefinition.cs | Web Access |
| Project: src\src\Aspire.Hosting.Native.Client\Aspire.Hosting.Native.Client.csproj (Aspire.Hosting.Native.Client) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Security.Cryptography; using System.Text.Json; namespace Aspire.Hosting.Native.Client; /// <summary>Contains offline-generated portable metadata and strongly typed provider dispatch.</summary> public sealed record NativeProviderDefinition(JsonElement Metadata, Func<NativeProviderContext, string, JsonElement, Task<JsonElement>> Dispatch); /// <summary>Associates an integration-owned resource with its authoritative native declaration lifetime.</summary> public abstract class NativeIntegrationResource(DeclaredResource declaration) : IAsyncDisposable { private int _released; /// <summary>Gets the native declaration that anchors this integration object lifetime.</summary> public DeclaredResource Declaration { get; } = declaration; internal bool IsReleased => Volatile.Read(ref _released) != 0; internal async Task ReleaseAsync() { if (Interlocked.Exchange(ref _released, 1) == 0) { await DisposeAsync().ConfigureAwait(false); } } /// <summary>Releases integration-owned local state when native lifetime retirement is acknowledged.</summary> public virtual ValueTask DisposeAsync() => ValueTask.CompletedTask; } /// <summary>Marks an exported integration method as an offline-projected native resource lifecycle handler.</summary> [AttributeUsage(AttributeTargets.Method)] public sealed class NativeResourceLifecycleAttribute(string phase) : Attribute { /// <summary>Gets initialize, allocated, ready, or retiring.</summary> public string Phase { get; } = phase; } /// <summary>Owns integration objects on one provider connection; never substitutes native model semantics.</summary> public sealed class NativeProviderContext(NativeClient client) { private readonly Dictionary<string, ResourceEntry> _resources = new(StringComparer.Ordinal); private readonly Dictionary<NativeIntegrationResource, (string Id, string Type)> _identities = new(ReferenceEqualityComparer.Instance); private readonly Lock _gate = new(); private readonly AsyncLocal<NativeClient.ProviderInvocation?> _invocation = new(); private bool _closed; private Task? _clear; /// <summary>Gets the transport-delivered cancellation for the current integration invocation.</summary> public CancellationToken CancellationToken => _invocation.Value?.CancellationToken ?? CancellationToken.None; internal void SetInvocation(NativeClient.ProviderInvocation invocation) { lock (_gate) { ObjectDisposedException.ThrowIf(_closed, this); _invocation.Value = invocation; } } internal void CompleteInvocation(NativeClient.ProviderInvocation invocation) { lock (_gate) { invocation.ResourceAccessClosed = true; foreach (var resource in invocation.Resources) { resource.Invocations.Remove(invocation); } invocation.Resources.Clear(); } } /// <summary>Gets the connection used by typed provider projections.</summary> public NativeClient Client { get; } = client; /// <summary>Invokes a callback through the server's original guest authority, not by forwarding a raw guest connection.</summary> public Task<JsonElement> InvokeGuestCallbackAsync(string callback, Dictionary<string, JsonElement> arguments) => Client.InvokeGuestCallbackAsync(callback, arguments); /// <summary>Publishes an integration-owned object with an explicit native declaration lifetime.</summary> public JsonElement PublishResource(NativeIntegrationResource resource, string typeId) { string id; lock (_gate) { if (_closed || resource.IsReleased) { throw new InvalidOperationException("An integration resource cannot be republished after native retirement."); } if (_identities.TryGetValue(resource, out var identity)) { if (_resources[identity.Id].Retirement is not null) { throw new InvalidOperationException("An integration resource cannot be republished during native retirement."); } if (identity.Type != typeId) { throw new InvalidOperationException("The integration resource type cannot change."); } id = identity.Id; } else { if (_resources.Count >= 4096) { throw new InvalidOperationException("Provider resource capacity was reached."); } id = RandomNumberGenerator.GetHexString(32); _resources.Add(id, new(resource)); _identities.Add(resource, (id, typeId)); } } // Provider resource wire identities include a delegated native lifetime. // The server validates and rewraps it into caller-owned authority. return JsonSerializer.SerializeToElement(new NativeProviderResourceWire(id, typeId, Client.GetHandle(resource.Declaration)), NativeProviderJsonContext.Default.NativeProviderResourceWire); } /// <summary>Resolves a provider-local object and leases it for the current invocation after native authority validation.</summary> public T ReadResource<T>(JsonElement handle) where T : NativeIntegrationResource { lock (_gate) { if (_closed || handle.ValueKind != JsonValueKind.Object || !handle.TryGetProperty("$handle", out var id) || id.ValueKind != JsonValueKind.String || !_resources.TryGetValue(id.GetString()!, out var value) || value.Retirement is not null || value.Resource is not T typed) { throw new InvalidOperationException("Unknown integration resource authority."); } if (_invocation.Value is { } invocation) { if (invocation.ResourceAccessClosed) { throw new InvalidOperationException("The integration invocation has already completed."); } // Resolution and retirement share this gate. The lease covers // awaited integration code, not just its synchronous lookup. value.Invocations.Add(invocation); invocation.Resources.Add(value); } return typed; } } internal Task<bool> ReleaseResourceAsync(string id) { lock (_gate) { if (!_resources.TryGetValue(id, out var resource)) { return Task.FromResult(true); } return resource.Retirement ??= ReleaseResourceCoreAsync(id, resource); } } private async Task<bool> ReleaseResourceCoreAsync(string id, ResourceEntry resource) { // Publish the shared retirement task before cancellation callbacks can // reenter the context; no new invocation can acquire this object. await Task.Yield(); NativeClient.ProviderInvocation[] active; lock (_gate) { active = resource.Invocations.ToArray(); } foreach (var invocation in active) { invocation.Cancel(); } await Task.WhenAll(active.Select(invocation => invocation.Completion.Task)).ConfigureAwait(false); try { await resource.Resource.ReleaseAsync().ConfigureAwait(false); return true; } finally { lock (_gate) { _resources.Remove(id); _identities.Remove(resource.Resource); } } } internal Task ClearAsync() { lock (_gate) { _closed = true; return _clear ??= Task.WhenAll(_resources.Select(resource => resource.Value.Retirement ??= ReleaseResourceCoreAsync(resource.Key, resource.Value)).ToArray()); } } /// <summary>Associates a provider object with its invocation leases and shared retirement.</summary> internal sealed class ResourceEntry(NativeIntegrationResource resource) { public NativeIntegrationResource Resource { get; } = resource; public HashSet<NativeClient.ProviderInvocation> Invocations { get; } = []; public Task<bool>? Retirement { get; set; } } } internal sealed record NativeProviderResourceWire( [property: System.Text.Json.Serialization.JsonPropertyName("$handle")] string Handle, [property: System.Text.Json.Serialization.JsonPropertyName("$type")] string Type, [property: System.Text.Json.Serialization.JsonPropertyName("$lifetime")] JsonElement Lifetime); [System.Text.Json.Serialization.JsonSerializable(typeof(NativeProviderResourceWire))] internal sealed partial class NativeProviderJsonContext : System.Text.Json.Serialization.JsonSerializerContext;