Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
43 changes: 43 additions & 0 deletions src/Aspire.Hosting.CodeGeneration.Java/Resources/Base.java
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,49 @@ private Handle ensureHandle() {
}
return resolvedHandle;
}

public int size() {
Object result = getClient().invokeCapability("Aspire.Hosting/Dict.count", Map.of("dict", ensureHandle().toJson()));
return ((Number) result).intValue();
}

@SuppressWarnings("unchecked")
public V get(K key) {
Map<String, Object> args = new HashMap<>();
args.put("dict", ensureHandle().toJson());
args.put("key", AspireClient.serializeValue(key));
return (V) getClient().invokeCapability("Aspire.Hosting/Dict.get", args);
Comment thread
sebastienros marked this conversation as resolved.
}

public void put(K key, V value) {
Map<String, Object> args = new HashMap<>();
args.put("dict", ensureHandle().toJson());
args.put("key", AspireClient.serializeValue(key));
args.put("value", AspireClient.serializeValue(value));
getClient().invokeCapability("Aspire.Hosting/Dict.set", args);
}

public boolean remove(K key) {
Map<String, Object> args = new HashMap<>();
args.put("dict", ensureHandle().toJson());
args.put("key", AspireClient.serializeValue(key));
Object result = getClient().invokeCapability("Aspire.Hosting/Dict.remove", args);
return Boolean.TRUE.equals(result);
}

public boolean containsKey(K key) {
Map<String, Object> args = new HashMap<>();
args.put("dict", ensureHandle().toJson());
args.put("key", AspireClient.serializeValue(key));
Object result = getClient().invokeCapability("Aspire.Hosting/Dict.has", args);
return Boolean.TRUE.equals(result);
}

@SuppressWarnings("unchecked")
public List<K> keys() {
Object result = getClient().invokeCapability("Aspire.Hosting/Dict.keys", Map.of("dict", ensureHandle().toJson()));
return (List<K>) result;
Comment thread
sebastienros marked this conversation as resolved.
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,8 @@ private string GetConcreteClassName(string typeId) => _wrapperClassNames.GetValu

private string GetPublicPromiseInterfaceName(string typeId) => GetPromiseInterfaceName(GetConcreteClassName(typeId));

/// <summary>
/// Checks if an AtsTypeRef represents a handle type.
/// </summary>
private static bool IsHandleType(AtsTypeRef? typeRef) =>
typeRef != null && typeRef.Category == AtsTypeCategory.Handle;
typeRef is { Category: AtsTypeCategory.Handle };

/// <summary>
/// Maps an AtsTypeRef to a TypeScript type using category-based dispatch.
Expand Down Expand Up @@ -615,34 +612,11 @@ private string GenerateAspireSdk(AtsContext context)
// Exclude DTO types - they have their own interfaces, not handle aliases
var dtoTypeIds = new HashSet<string>(dtoTypes.Select(d => d.TypeId));
var typeIds = new HashSet<string>();
foreach (var cap in capabilities)
foreach (var typeId in CollectAllReferencedTypes(capabilities).Keys)
{
if (!string.IsNullOrEmpty(cap.TargetTypeId) && !dtoTypeIds.Contains(cap.TargetTypeId))
if (!dtoTypeIds.Contains(typeId))
{
typeIds.Add(cap.TargetTypeId);
}
if (IsHandleType(cap.ReturnType) && !dtoTypeIds.Contains(cap.ReturnType!.TypeId))
{
typeIds.Add(GetReturnTypeId(cap)!);
}
// Add parameter type IDs (for types like IResourceBuilder<IResource>)
foreach (var param in cap.Parameters)
{
if (IsHandleType(param.Type) && !dtoTypeIds.Contains(param.Type!.TypeId))
{
typeIds.Add(param.Type!.TypeId);
}
// Also collect callback parameter types
if (param.IsCallback && param.CallbackParameters != null)
{
foreach (var cbParam in param.CallbackParameters)
{
if (IsHandleType(cbParam.Type) && !dtoTypeIds.Contains(cbParam.Type.TypeId))
{
typeIds.Add(cbParam.Type.TypeId);
}
}
}
typeIds.Add(typeId);
}
}

Expand Down Expand Up @@ -2186,25 +2160,7 @@ private void GenerateCallbackBody(AtsParameterInfo callbackParam, IReadOnlyList<
{
// Single parameter callback
var cbParam = callbackParameters[0];
var tsType = MapTypeRefToTypeScript(cbParam.Type);
var cbTypeId = cbParam.Type.TypeId;

if (cbTypeId == AtsConstants.CancellationToken)
{
WriteLine($"{bodyIndent}const {cbParam.Name} = CancellationToken.fromValue({cbParam.Name}Data);");
}
else if (_wrapperClassNames.TryGetValue(cbTypeId, out var wrapperClassName))
{
// For types with wrapper classes, create an instance of the wrapper
var handleType = GetHandleTypeName(cbTypeId);
WriteLine($"{bodyIndent}const {cbParam.Name}Handle = wrapIfHandle({cbParam.Name}Data) as {handleType};");
WriteLine($"{bodyIndent}const {cbParam.Name} = new {GetImplementationClassName(wrapperClassName)}({cbParam.Name}Handle, this._client);");
}
else
{
// For raw handle types, just wrap and cast
WriteLine($"{bodyIndent}const {cbParam.Name} = wrapIfHandle({cbParam.Name}Data) as {tsType};");
}
GenerateCallbackParameterConversion(cbParam, $"{cbParam.Name}Data");

WriteLine($"{bodyIndent}{returnPrefix}await {callbackName}({cbParam.Name});");
}
Expand All @@ -2214,33 +2170,46 @@ private void GenerateCallbackBody(AtsParameterInfo callbackParam, IReadOnlyList<
for (var i = 0; i < callbackParameters.Count; i++)
{
var cbParam = callbackParameters[i];
var tsType = MapTypeRefToTypeScript(cbParam.Type);
var cbTypeId = cbParam.Type.TypeId;
var callbackArgName = $"{cbParam.Name}Data";

if (cbTypeId == AtsConstants.CancellationToken)
{
WriteLine($"{bodyIndent}const {cbParam.Name} = CancellationToken.fromValue({callbackArgName});");
}
else if (_wrapperClassNames.TryGetValue(cbTypeId, out var wrapperClassName))
{
// For types with wrapper classes, create an instance of the wrapper
var handleType = GetHandleTypeName(cbTypeId);
WriteLine($"{bodyIndent}const {cbParam.Name}Handle = wrapIfHandle({callbackArgName}) as {handleType};");
WriteLine($"{bodyIndent}const {cbParam.Name} = new {GetImplementationClassName(wrapperClassName)}({cbParam.Name}Handle, this._client);");
}
else
{
// For raw handle types, just wrap and cast
WriteLine($"{bodyIndent}const {cbParam.Name} = wrapIfHandle({callbackArgName}) as {tsType};");
}
GenerateCallbackParameterConversion(cbParam, callbackArgName);
callArgs.Add(cbParam.Name);
}

WriteLine($"{bodyIndent}{returnPrefix}await {callbackName}({string.Join(", ", callArgs)});");
}
}

private void GenerateCallbackParameterConversion(AtsCallbackParameterInfo callbackParameter, string callbackArgName)
{
var tsType = MapTypeRefToTypeScript(callbackParameter.Type);
var cbTypeId = callbackParameter.Type.TypeId;

if (cbTypeId == AtsConstants.CancellationToken)
{
WriteLine($" const {callbackParameter.Name} = CancellationToken.fromValue({callbackArgName});");
}
else if (IsDictionaryType(callbackParameter.Type) && !callbackParameter.Type.IsReadOnly)
{
var keyType = MapTypeRefToTypeScript(callbackParameter.Type.KeyType);
var valueType = MapTypeRefToTypeScript(callbackParameter.Type.ValueType);
var handleType = GetHandleTypeName(cbTypeId);

WriteLine($" const {callbackParameter.Name}Handle = wrapIfHandle({callbackArgName}) as {handleType};");
WriteLine($" const {callbackParameter.Name} = new AspireDict<{keyType}, {valueType}>({callbackParameter.Name}Handle, this._client, '{cbTypeId}');");
}
else if (_wrapperClassNames.TryGetValue(cbTypeId, out var wrapperClassName))
{
var handleType = GetHandleTypeName(cbTypeId);
WriteLine($" const {callbackParameter.Name}Handle = wrapIfHandle({callbackArgName}) as {handleType};");
WriteLine($" const {callbackParameter.Name} = new {GetImplementationClassName(wrapperClassName)}({callbackParameter.Name}Handle, this._client);");
}
else
{
WriteLine($" const {callbackParameter.Name} = wrapIfHandle({callbackArgName}) as {tsType};");
}
}

private void GenerateConnectionHelper()
{
var builderHandle = GetHandleTypeName(AtsConstants.BuilderTypeId);
Expand Down
7 changes: 6 additions & 1 deletion src/Aspire.Hosting.Docker/CapturedEnvironmentVariable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ namespace Aspire.Hosting.Docker;
/// Represents a captured environment variable that will be written to the .env file
/// adjacent to the Docker Compose file.
/// </summary>
[AspireExport(ExposeProperties = true)]
public sealed class CapturedEnvironmentVariable
{
/// <summary>
/// Gets the name of the environment variable.
/// </summary>
[AspireExportIgnore(Reason = "The dictionary key already identifies the captured environment variable in polyglot callbacks.")]
public required string Name { get; init; }

/// <summary>
Expand All @@ -29,14 +31,17 @@ public sealed class CapturedEnvironmentVariable
/// <summary>
/// Gets or sets the source object that originated this environment variable.
/// This could be a <see cref="ParameterResource"/>,
/// <see cref="ContainerMountAnnotation"/>, or other source types.
/// <see cref="ContainerMountAnnotation"/>, <see cref="ContainerImageReference"/>,
/// or <see cref="ContainerPortReference"/>.
/// </summary>
[AspireUnion(typeof(ParameterResource), typeof(ContainerMountAnnotation), typeof(ContainerImageReference), typeof(ContainerPortReference))]
public object? Source { get; set; }
Comment thread
sebastienros marked this conversation as resolved.

/// <summary>
/// Gets or sets the resource that this environment variable is associated with.
/// This is useful when the source is an annotation on a resource, allowing you to
/// identify which resource this environment variable is related to.
/// </summary>
[AspireExportIgnore(Reason = "Resource is provenance metadata only; exporting it here would pull the broader IResource surface into the callback even though polyglot configureEnvFile only needs to mutate Description and DefaultValue.")]
public IResource? Resource { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,12 @@ public static IResourceBuilder<DockerComposeEnvironmentResource> ConfigureCompos
/// <param name="configure">A method that can be used for customizing the captured environment variables.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
/// <remarks>
/// This method is not available in polyglot app hosts.
/// <para>
/// This callback is invoked during the prepare phase, allowing programmatic modification of the environment variables
/// that will be written to the .env file adjacent to the Docker Compose file.
/// that will be written to the environment-specific <c>.env</c> file adjacent to the Docker Compose file.
/// </para>
/// </remarks>
[AspireExportIgnore(Reason = "Action<IDictionary<string, CapturedEnvironmentVariable>> callbacks are not ATS-compatible.")]
[AspireExport(Description = "Configures the captured environment variables written to the Docker Compose .env file")]
public static IResourceBuilder<DockerComposeEnvironmentResource> ConfigureEnvFile(this IResourceBuilder<DockerComposeEnvironmentResource> builder, Action<IDictionary<string, CapturedEnvironmentVariable>> configure)
{
ArgumentNullException.ThrowIfNull(builder);
Expand Down
12 changes: 11 additions & 1 deletion src/Aspire.Hosting.Integration.Analyzers/AspireExportAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1306,11 +1306,21 @@ private static bool IsAtsCompatibleCollectionType(
INamedTypeSymbol? aspireExportAttribute,
HashSet<ITypeSymbol>? currentAssemblyExportedTypes)
{
if (type is not INamedTypeSymbol namedType || !namedType.IsGenericType)
if (type is not INamedTypeSymbol namedType)
{
return false;
}

if (!namedType.IsGenericType)
{
return namedType is
{
ContainingNamespace.Name: "Collections",
ContainingNamespace.ContainingNamespace.Name: "System",
Name: "IDictionary" or "IList"
};
}

// Dictionary<K,V> and IDictionary<K,V>
if (TryMatchGenericType(type, wellKnownTypes, WellKnownTypeData.WellKnownType.System_Collections_Generic_Dictionary_2) ||
TryMatchGenericType(type, wellKnownTypes, WellKnownTypeData.WellKnownType.System_Collections_Generic_IDictionary_2))
Expand Down
35 changes: 35 additions & 0 deletions src/Aspire.Hosting.RemoteHost/AtsCapabilityScanner.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.Collections.Concurrent;
using System.Reflection;
using System.Xml.Linq;
Expand Down Expand Up @@ -1807,6 +1808,16 @@ private static (IReadOnlyList<AtsCallbackParameterInfo>? Parameters, AtsTypeRef?
return AtsConstants.Any;
}

if (type == typeof(IDictionary))
{
return AtsConstants.DictTypeId(AtsConstants.String, AtsConstants.Any);
}

if (type == typeof(IList))
{
return AtsConstants.ListTypeId(AtsConstants.Any);
}

// Handle enum types
if (type.IsEnum)
{
Expand Down Expand Up @@ -2074,6 +2085,30 @@ private static bool IsResourceBuilderType(Type type)
return new AtsTypeRef { TypeId = AtsConstants.Any, ClrType = type, Category = AtsTypeCategory.Primitive };
}

if (type == typeof(IDictionary))
{
return new AtsTypeRef
{
TypeId = AtsConstants.DictTypeId(AtsConstants.String, AtsConstants.Any),
ClrType = type,
Category = AtsTypeCategory.Dict,
KeyType = new AtsTypeRef { TypeId = AtsConstants.String, ClrType = typeof(string), Category = AtsTypeCategory.Primitive },
ValueType = new AtsTypeRef { TypeId = AtsConstants.Any, ClrType = typeof(object), Category = AtsTypeCategory.Primitive },
IsReadOnly = false
};
}

if (type == typeof(IList))
{
return new AtsTypeRef
{
TypeId = AtsConstants.ListTypeId(AtsConstants.Any),
ClrType = type,
Category = AtsTypeCategory.List,
ElementType = new AtsTypeRef { TypeId = AtsConstants.Any, ClrType = typeof(object), Category = AtsTypeCategory.Primitive }
};
}

// Handle enum types
if (type.IsEnum)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public ContainerImageReference(IResource resource)
public string ValueExpression => $"{{{Resource.Name}.containerImage}}";

/// <inheritdoc/>
[global::Aspire.Hosting.AspireExportIgnore(Reason = "Reference enumeration is not needed in the ATS surface for container image provenance.")]
public IEnumerable<object> References => [Resource];

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class ContainerPortReference(IResource resource) : IManifestExpressionPro
public string ValueExpression => $"{{{Resource.Name}.containerPort}}";

/// <inheritdoc/>
[global::Aspire.Hosting.AspireExportIgnore(Reason = "Reference enumeration is not needed in the ATS surface for container port provenance.")]
public IEnumerable<object> References => [Resource];

ValueTask<string?> IValueProvider.GetValueAsync(CancellationToken cancellationToken)
Expand Down
3 changes: 3 additions & 0 deletions src/Aspire.Hosting/Ats/AtsTypeMappings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
[assembly: AspireExport(typeof(ExecutableResource))]
[assembly: AspireExport(typeof(ProjectResource))]
[assembly: AspireExport(typeof(ParameterResource))]
[assembly: AspireExport(typeof(ContainerMountAnnotation), ExposeProperties = true)]
[assembly: AspireExport(typeof(ContainerImageReference), ExposeProperties = true)]
[assembly: AspireExport(typeof(ContainerPortReference), ExposeProperties = true)]

// Service types
[assembly: AspireExport(typeof(IServiceProvider))]
Expand Down
Loading
Loading