File: AtsCapabilityScannerTests.cs
Web Access
Project: src\tests\Aspire.Hosting.RemoteHost.Tests\Aspire.Hosting.RemoteHost.Tests.csproj (Aspire.Hosting.RemoteHost.Tests)
// 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.Reflection;
using System.Reflection.Emit;
using System.Text.Json.Nodes;
using Aspire.Hosting.ApplicationModel;
using Aspire.TypeSystem;
using Xunit;
 
namespace Aspire.Hosting.RemoteHost.Tests;
 
public class AtsCapabilityScannerTests
{
    #region MapToAtsTypeId Tests
 
    [Fact]
    public void MapToAtsTypeId_String_ReturnsString()
    {
        var result = AtsCapabilityScanner.MapToAtsTypeId(typeof(string));
 
        Assert.Equal("string", result);
    }
 
    [Fact]
    public void MapToAtsTypeId_Int32_ReturnsNumber()
    {
        var result = AtsCapabilityScanner.MapToAtsTypeId(typeof(int));
 
        Assert.Equal("number", result);
    }
 
    [Fact]
    public void MapToAtsTypeId_Boolean_ReturnsBoolean()
    {
        var result = AtsCapabilityScanner.MapToAtsTypeId(typeof(bool));
 
        Assert.Equal("boolean", result);
    }
 
    [Fact]
    public void MapToAtsTypeId_Void_ReturnsNull()
    {
        var result = AtsCapabilityScanner.MapToAtsTypeId(typeof(void));
 
        Assert.Null(result);
    }
 
    [Fact]
    public void MapToAtsTypeId_Task_ReturnsNull()
    {
        var result = AtsCapabilityScanner.MapToAtsTypeId(typeof(Task));
 
        Assert.Null(result);
    }
 
    [Fact]
    public void MapToAtsTypeId_TaskOfString_ReturnsString()
    {
        var result = AtsCapabilityScanner.MapToAtsTypeId(typeof(Task<string>));
 
        Assert.Equal("string", result);
    }
 
    [Fact]
    public void MapToAtsTypeId_TaskOfInt_ReturnsNumber()
    {
        var result = AtsCapabilityScanner.MapToAtsTypeId(typeof(Task<int>));
 
        Assert.Equal("number", result);
    }
 
    [Fact]
    public void MapToAtsTypeId_NullableInt_ReturnsNumber()
    {
        var result = AtsCapabilityScanner.MapToAtsTypeId(typeof(int?));
 
        Assert.Equal("number", result);
    }
 
    [Fact]
    public void MapToAtsTypeId_StringArray_ReturnsStringArray()
    {
        var result = AtsCapabilityScanner.MapToAtsTypeId(typeof(string[]));
 
        Assert.Equal("string[]", result);
    }
 
    [Fact]
    public void MapToAtsTypeId_IntArray_ReturnsNumberArray()
    {
        var result = AtsCapabilityScanner.MapToAtsTypeId(typeof(int[]));
 
        Assert.Equal("number[]", result);
    }
 
    [Fact]
    public void MapToAtsTypeId_IEnumerableOfString_ReturnsStringArray()
    {
        var result = AtsCapabilityScanner.MapToAtsTypeId(typeof(IEnumerable<string>));
 
        Assert.Equal("string[]", result);
    }
 
    [Fact]
    public void MapToAtsTypeId_IResourceBuilder_ExtractsResourceType()
    {
        var result = AtsCapabilityScanner.MapToAtsTypeId(typeof(IResourceBuilder<TestResource>));
 
        // Should derive type ID from TestResource's full name
        // Format: {AssemblyName}/{FullTypeName}
        Assert.Equal("Aspire.Hosting.RemoteHost.Tests/Aspire.Hosting.RemoteHost.Tests.AtsCapabilityScannerTests+TestResource", result);
    }
 
    [Fact]
    public void MapToAtsTypeId_UnknownType_ReturnsNull()
    {
        var result = AtsCapabilityScanner.MapToAtsTypeId(typeof(AtsCapabilityScannerTests));
 
        // Unknown types return null (capabilities with unknown types are skipped)
        Assert.Null(result);
    }
 
    [Fact]
    public void MapToAtsTypeId_ObjectType_ReturnsAny()
    {
        var result = AtsCapabilityScanner.MapToAtsTypeId(typeof(object));
 
        // System.Object maps to 'any'
        Assert.Equal("any", result);
    }
 
    [Fact]
    public void MapToAtsTypeId_NonGenericIDictionary_ReturnsStringAnyDict()
    {
        var result = AtsCapabilityScanner.MapToAtsTypeId(typeof(IDictionary));
 
        Assert.Equal("Aspire.Hosting/Dict<string,any>", result);
    }
 
    [Fact]
    public void MapToAtsTypeId_NonGenericIList_ReturnsAnyList()
    {
        var result = AtsCapabilityScanner.MapToAtsTypeId(typeof(IList));
 
        Assert.Equal("Aspire.Hosting/List<any>", result);
    }
 
    [Fact]
    public void ScanAssembly_IEnumerableCapability_UsesArrayTypes()
    {
        var result = AtsCapabilityScanner.ScanAssembly(typeof(AtsCapabilityScannerTests).Assembly);
 
        var enumerableParameterCapability = Assert.Single(result.Capabilities,
            c => c.CapabilityId.EndsWith("/testEnumerableParameter", StringComparison.Ordinal));
        var itemsParameter = Assert.Single(enumerableParameterCapability.Parameters);
        var itemsType = Assert.IsType<AtsTypeRef>(itemsParameter.Type);
        Assert.Equal("string[]", itemsType.TypeId);
        Assert.Equal(AtsTypeCategory.Array, itemsType.Category);
 
        var enumerableReturnCapability = Assert.Single(result.Capabilities,
            c => c.CapabilityId.EndsWith("/testEnumerableReturn", StringComparison.Ordinal));
        Assert.Equal("string[]", enumerableReturnCapability.ReturnType.TypeId);
        Assert.Equal(AtsTypeCategory.Array, enumerableReturnCapability.ReturnType.Category);
    }
 
    [Fact]
    public void ScanAssembly_UnionCapability_CollectsEnumTypesFromUnionMembers()
    {
        var result = AtsCapabilityScanner.ScanAssembly(typeof(AtsCapabilityScannerTests).Assembly);
 
        var capability = Assert.Single(result.Capabilities,
            c => c.CapabilityId.EndsWith("/testUnionEnumParameter", StringComparison.Ordinal));
        var parameter = Assert.Single(capability.Parameters);
 
        Assert.Equal(AtsTypeCategory.Union, parameter.Type?.Category);
        Assert.Contains(parameter.Type!.UnionTypes!, type => type.ClrType == typeof(TestUnionEnum));
        Assert.Contains(parameter.Type.UnionTypes!, type => type.TypeId == AtsConstants.String);
        Assert.Contains(result.EnumTypes, type => type.ClrType == typeof(TestUnionEnum));
    }
 
    #endregion
 
    #region DeriveMethodName Tests
 
    [Fact]
    public void DeriveMethodName_SimpleCapabilityId_ReturnsMethodName()
    {
        var result = AtsCapabilityScanner.DeriveMethodName("Aspire.Hosting/createBuilder");
 
        Assert.Equal("createBuilder", result);
    }
 
    [Fact]
    public void DeriveMethodName_NestedCapabilityId_ReturnsMethodName()
    {
        var result = AtsCapabilityScanner.DeriveMethodName("Aspire.Hosting.Redis/addRedis");
 
        Assert.Equal("addRedis", result);
    }
 
    [Fact]
    public void DeriveMethodName_NoSlash_ReturnsEntireId()
    {
        var result = AtsCapabilityScanner.DeriveMethodName("withEnvironment");
 
        Assert.Equal("withEnvironment", result);
    }
 
    #endregion
 
    #region DerivePackage Tests
 
    [Fact]
    public void DerivePackage_SimpleCapabilityId_ReturnsPackage()
    {
        var result = AtsCapabilityScanner.DerivePackage("Aspire.Hosting/createBuilder");
 
        Assert.Equal("Aspire.Hosting", result);
    }
 
    [Fact]
    public void DerivePackage_NestedCapabilityId_ReturnsPackage()
    {
        var result = AtsCapabilityScanner.DerivePackage("Aspire.Hosting.Redis/addRedis");
 
        Assert.Equal("Aspire.Hosting.Redis", result);
    }
 
    #endregion
 
    #region Assembly-Level AspireExport Tests
 
    [Fact]
    public void ScanAssembly_AssemblyLevelExport_AppearsInHandleTypes()
    {
        // Regression test: assembly-level [AspireExport(typeof(T))] attributes must be
        // discovered and included in HandleTypes so they participate in Unknown→Handle resolution.
        // The Aspire.Hosting assembly exports CancellationToken at assembly level.
        var hostingAssembly = typeof(DistributedApplication).Assembly;
        var result = AtsCapabilityScanner.ScanAssembly(hostingAssembly);
 
        // ContainerApp types are exported via assembly-level attributes in AppContainers,
        // but CancellationToken is exported in Aspire.Hosting's AtsTypeMappings.cs
        var cancellationTokenType = result.HandleTypes
            .FirstOrDefault(t => t.AtsTypeId.Contains("CancellationToken"));
 
        Assert.NotNull(cancellationTokenType);
    }
 
    [Fact]
    public void ScanAssembly_HostingAssembly_CoreFrameworkAndLifecycleCapabilitiesAreRegistered()
    {
        var hostingAssembly = typeof(DistributedApplication).Assembly;
        var result = AtsCapabilityScanner.ScanAssembly(hostingAssembly);
 
        var expectedCapabilities = new[]
        {
            "Aspire.Hosting/getSection",
            "Aspire.Hosting/getChildren",
            "Aspire.Hosting/exists",
            "Aspire.Hosting/isProduction",
            "Aspire.Hosting/isStaging",
            "Aspire.Hosting/isEnvironment",
            "Aspire.Hosting/subscribeBeforeStart",
            "Aspire.Hosting/subscribeAfterResourcesCreated",
            "Aspire.Hosting/onBeforeResourceStarted",
            "Aspire.Hosting/onResourceStopped",
            "Aspire.Hosting/onConnectionStringAvailable",
            "Aspire.Hosting/onInitializeResource",
            "Aspire.Hosting/onResourceEndpointsAllocated",
            "Aspire.Hosting/onResourceReady",
            "Aspire.Hosting/getLoggerFactory",
            "Aspire.Hosting/createLogger",
            "Aspire.Hosting/getResourceLoggerService",
            "Aspire.Hosting/getResourceCommandService",
            "Aspire.Hosting/executeResourceCommand",
            "Aspire.Hosting/getResourceNotificationService",
            "Aspire.Hosting/getDistributedApplicationModel",
            "Aspire.Hosting/getResources",
            "Aspire.Hosting/findResourceByName",
            "Aspire.Hosting/getUserSecretsManager",
            "Aspire.Hosting/getEventing",
            "Aspire.Hosting/saveStateJson"
        };
 
        foreach (var expectedCapability in expectedCapabilities)
        {
            Assert.Contains(result.Capabilities, capability => capability.CapabilityId == expectedCapability);
        }
    }
 
    [Fact]
    public void ScanAssembly_HostingAssembly_ExportsResourceCommandWithResourceUnionAndArguments()
    {
        var hostingAssembly = typeof(DistributedApplication).Assembly;
        var result = AtsCapabilityScanner.ScanAssembly(hostingAssembly);
 
        var capability = Assert.Single(result.Capabilities,
            capability => capability.CapabilityId == "Aspire.Hosting/executeResourceCommand");
 
        Assert.Equal("executeCommandAsync", capability.MethodName);
        Assert.Equal("resourceCommandService", capability.TargetParameterName);
        Assert.Equal(4, capability.Parameters.Count);
 
        var resourceParameter = capability.Parameters[0];
        Assert.Equal("resource", resourceParameter.Name);
        Assert.Equal(AtsTypeCategory.Union, resourceParameter.Type?.Category);
        Assert.Contains(resourceParameter.Type!.UnionTypes!, type => type.TypeId == "string");
        Assert.Contains(resourceParameter.Type.UnionTypes!, type => type.TypeId == "Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource");
 
        var argumentsParameter = capability.Parameters.Single(parameter => parameter.Name == "arguments");
        Assert.True(argumentsParameter.IsOptional);
        Assert.Equal(AtsTypeCategory.Dict, argumentsParameter.Type?.Category);
        Assert.True(argumentsParameter.Type?.IsReadOnly);
    }
 
    [Fact]
    public void ScanAssembly_HostingAssembly_ExportsExpectedHandleTypesAndInstanceMembers()
    {
        var hostingAssembly = typeof(DistributedApplication).Assembly;
        var result = AtsCapabilityScanner.ScanAssembly(hostingAssembly);
 
        var expectedHandleTypes = new[]
        {
            "IConfigurationSection",
            "ILogger",
            "ILoggerFactory",
            "ResourceCommandService",
            "DistributedApplicationModel",
            "IDistributedApplicationEventing",
            "BeforeStartEvent",
            "AfterResourcesCreatedEvent",
            "BeforeResourceStartedEvent",
            "ConnectionStringAvailableEvent",
            "InitializeResourceEvent",
            "ResourceEndpointsAllocatedEvent",
            "ResourceReadyEvent",
            "ResourceStoppedEvent",
            "IUserSecretsManager",
            "IReportingStep",
            "IReportingTask",
            "PipelineContext",
            "PipelineStepFactoryContext",
            "PipelineSummary"
        };
 
        foreach (var expectedHandleType in expectedHandleTypes)
        {
            Assert.Contains(result.HandleTypes, type => type.AtsTypeId.Contains(expectedHandleType, StringComparison.Ordinal));
        }
 
        Assert.Contains(result.Capabilities, capability => capability.CapabilityId.EndsWith("/BeforeStartEvent.services", StringComparison.Ordinal));
        Assert.Contains(result.Capabilities, capability => capability.CapabilityId.EndsWith("/IUserSecretsManager.trySetSecret", StringComparison.Ordinal));
        Assert.Contains(result.Capabilities, capability => capability.CapabilityId.EndsWith("/PipelineStepContext.reportingStep", StringComparison.Ordinal));
        Assert.Contains(result.Capabilities, capability => capability.CapabilityId.EndsWith("/PipelineSummary.add", StringComparison.Ordinal));
    }
 
    [Fact]
    public void ScanAssembly_HostingAssembly_ExportsWithHttpCommandCapabilityWithAtsFriendlyOptions()
    {
        var hostingAssembly = typeof(DistributedApplication).Assembly;
        var result = AtsCapabilityScanner.ScanAssembly(hostingAssembly);
 
        var capability = Assert.Single(result.Capabilities,
            c => c.CapabilityId == "Aspire.Hosting/withHttpCommand");
 
        Assert.Equal("withHttpCommand", capability.MethodName);
        Assert.Equal(3, capability.Parameters.Count);
        Assert.DoesNotContain(capability.Parameters, p => p.Name == "endpointName");
        Assert.DoesNotContain(capability.Parameters, p => p.Name == "commandName");
 
        var optionsParameter = Assert.Single(capability.Parameters, p => p.Name == "options");
        Assert.Equal("options", optionsParameter.Name);
        Assert.NotNull(optionsParameter.Type);
        Assert.Equal(AtsTypeCategory.Dto, optionsParameter.Type.Category);
        Assert.Equal(AtsTypeMapping.DeriveTypeId(typeof(HttpCommandExportOptions)), optionsParameter.Type.TypeId);
 
        var dto = Assert.Single(result.DtoTypes, d => d.TypeId == AtsTypeMapping.DeriveTypeId(typeof(HttpCommandExportOptions)));
        Assert.Equal(nameof(HttpCommandExportOptions), dto.Name);
        var commandOptionsProperty = Assert.Single(dto.Properties, p => p.Name == nameof(HttpCommandExportOptions.CommandOptions));
        Assert.True(commandOptionsProperty.IsOptional);
        Assert.Contains(dto.Properties, p => p.Name == nameof(HttpCommandExportOptions.CommandName));
        Assert.Contains(dto.Properties, p => p.Name == nameof(HttpCommandExportOptions.EndpointName));
        Assert.Contains(dto.Properties, p => p.Name == nameof(HttpCommandExportOptions.MethodName));
        Assert.Contains(dto.Properties, p => p.Name == nameof(HttpCommandExportOptions.ResultMode));
        var prepareRequestProperty = Assert.Single(dto.Properties, p => p.Name == nameof(HttpCommandExportOptions.PrepareRequest));
        Assert.True(prepareRequestProperty.IsCallback);
        Assert.True(prepareRequestProperty.IsOptional);
 
        var callbackParameter = Assert.Single(prepareRequestProperty.CallbackParameters!);
        Assert.Equal(AtsTypeMapping.DeriveTypeId(typeof(HttpCommandPrepareRequestContext)), callbackParameter.Type.TypeId);
        Assert.Equal(AtsTypeCategory.Handle, callbackParameter.Type.Category);
        Assert.Equal(AtsTypeMapping.DeriveTypeId(typeof(HttpCommandRequestExportData)), prepareRequestProperty.CallbackReturnType?.TypeId);
        Assert.Equal(AtsTypeCategory.Dto, prepareRequestProperty.CallbackReturnType?.Category);
 
        Assert.DoesNotContain(dto.Properties, p => p.Name == "Parameter");
        Assert.DoesNotContain(dto.Properties, p => p.Name == nameof(HttpCommandOptions.HttpClientName));
        Assert.DoesNotContain(dto.Properties, p => p.Name == nameof(HttpCommandOptions.Method));
        Assert.DoesNotContain(dto.Properties, p => p.Name == nameof(HttpCommandOptions.EndpointSelector));
        Assert.DoesNotContain(dto.Properties, p => p.Name == nameof(HttpCommandOptions.GetCommandResult));
 
        Assert.DoesNotContain(result.Capabilities,
            c => c.CapabilityId == "Aspire.Hosting/withHttpCommandPrepareRequest");
 
        var requestDataDto = Assert.Single(result.DtoTypes, d => d.TypeId == AtsTypeMapping.DeriveTypeId(typeof(HttpCommandRequestExportData)));
        Assert.Equal(nameof(HttpCommandRequestExportData), requestDataDto.Name);
        Assert.Contains(requestDataDto.Properties, p => p.Name == nameof(HttpCommandRequestExportData.MethodName));
        Assert.Contains(requestDataDto.Properties, p => p.Name == nameof(HttpCommandRequestExportData.Headers));
        Assert.Contains(requestDataDto.Properties, p => p.Name == nameof(HttpCommandRequestExportData.Content));
        Assert.Contains(requestDataDto.Properties, p => p.Name == nameof(HttpCommandRequestExportData.ContentType));
    }
 
    [Fact]
    public void ScanAssembly_DerivedExportedType_DoesNotRegenerateInheritedProperties()
    {
        var result = AtsCapabilityScanner.ScanAssembly(typeof(AtsCapabilityScannerTests).Assembly);
 
        var baseNameCapability = Assert.Single(result.Capabilities,
            c => c.CapabilityId.EndsWith("/BaseExportedProperties.name", StringComparison.Ordinal));
 
        Assert.Contains(baseNameCapability.ExpandedTargetTypes,
            t => t.TypeId == AtsTypeMapping.DeriveTypeId(typeof(DerivedExportedProperties)));
        Assert.Contains(result.Capabilities,
            c => c.CapabilityId.EndsWith("/DerivedExportedProperties.framework", StringComparison.Ordinal));
        Assert.DoesNotContain(result.Capabilities,
            c => c.CapabilityId.EndsWith("/DerivedExportedProperties.name", StringComparison.Ordinal));
        Assert.DoesNotContain(result.Diagnostics,
            d => d.Message.Contains(nameof(DerivedExportedProperties), StringComparison.Ordinal)
                && d.Message.Contains("has collisions", StringComparison.Ordinal));
    }
 
    [Fact]
    public void ScanAssembly_ExposeProperties_DoesNotGenerateSettersForInitOnlyProperties()
    {
        var result = AtsCapabilityScanner.ScanAssembly(typeof(AtsCapabilityScannerTests).Assembly);
        var capabilityPrefix = "Aspire.Hosting.RemoteHost.Tests/InitOnlyExportedProperties.";
 
        var nameGetter = Assert.Single(result.Capabilities,
            c => c.CapabilityId == capabilityPrefix + "name");
        Assert.Equal(AtsCapabilityKind.PropertyGetter, nameGetter.CapabilityKind);
        Assert.Equal(AtsConstants.String, nameGetter.ReturnType.TypeId);
 
        var descriptionGetter = Assert.Single(result.Capabilities,
            c => c.CapabilityId == capabilityPrefix + "description");
        Assert.Equal(AtsCapabilityKind.PropertyGetter, descriptionGetter.CapabilityKind);
        Assert.Equal(AtsConstants.String, descriptionGetter.ReturnType.TypeId);
        Assert.True(descriptionGetter.ReturnType.IsNullable);
 
        var mutableSetter = Assert.Single(result.Capabilities,
            c => c.CapabilityId == capabilityPrefix + "setMutableLabel");
        Assert.Equal(AtsCapabilityKind.PropertySetter, mutableSetter.CapabilityKind);
 
        Assert.DoesNotContain(result.Capabilities,
            c => c.CapabilityId == capabilityPrefix + "setName");
        Assert.DoesNotContain(result.Capabilities,
            c => c.CapabilityId == capabilityPrefix + "setDescription");
    }
 
    [Fact]
    public void ScanAssembly_DtoNullableScalarProperties_SetTypeRefNullability()
    {
        var result = AtsCapabilityScanner.ScanAssembly(typeof(AtsCapabilityScannerTests).Assembly);
 
        var dto = Assert.Single(result.DtoTypes, d => d.ClrType == typeof(NullableScalarDto));
 
        var nullableString = Assert.Single(dto.Properties, p => p.Name == nameof(NullableScalarDto.NullableString));
        Assert.Equal(AtsConstants.String, nullableString.Type.TypeId);
        Assert.True(nullableString.Type.IsNullable);
        Assert.False(nullableString.IsOptional);
 
        var requiredString = Assert.Single(dto.Properties, p => p.Name == nameof(NullableScalarDto.RequiredString));
        Assert.Equal(AtsConstants.String, requiredString.Type.TypeId);
        Assert.NotEqual(true, requiredString.Type.IsNullable);
        Assert.False(requiredString.IsOptional);
 
        var nullableNumber = Assert.Single(dto.Properties, p => p.Name == nameof(NullableScalarDto.NullableNumber));
        Assert.Equal(AtsConstants.Number, nullableNumber.Type.TypeId);
        Assert.True(nullableNumber.Type.IsNullable);
        Assert.True(nullableNumber.IsOptional);
 
        var requiredNumber = Assert.Single(dto.Properties, p => p.Name == nameof(NullableScalarDto.RequiredNumber));
        Assert.Equal(AtsConstants.Number, requiredNumber.Type.TypeId);
        Assert.NotEqual(true, requiredNumber.Type.IsNullable);
        Assert.False(requiredNumber.IsOptional);
    }
 
    [Fact]
    public void ScanAssembly_TargetSpecificMethodShadowsGenericExpandedMethodOnlyForThatTarget()
    {
        var result = AtsCapabilityScanner.ScanAssembly(typeof(AtsCapabilityScannerTests).Assembly);
        var shadowedTypeId = AtsTypeMapping.DeriveTypeId(typeof(ShadowedEnvironmentResource));
        var otherTypeId = AtsTypeMapping.DeriveTypeId(typeof(OtherEnvironmentResource));
 
        var genericCapability = Assert.Single(result.Capabilities,
            c => c.CapabilityId.EndsWith("/shadowedExporter", StringComparison.Ordinal));
        var specificCapability = Assert.Single(result.Capabilities,
            c => c.CapabilityId.EndsWith("/specificShadowedExporter", StringComparison.Ordinal));
 
        Assert.Equal("shadowedExporter", genericCapability.MethodName);
        Assert.Equal("shadowedExporter", specificCapability.MethodName);
        Assert.DoesNotContain(genericCapability.ExpandedTargetTypes, t => t.TypeId == shadowedTypeId);
        Assert.Contains(genericCapability.ExpandedTargetTypes, t => t.TypeId == otherTypeId);
        Assert.Contains(specificCapability.ExpandedTargetTypes, t => t.TypeId == shadowedTypeId);
        Assert.DoesNotContain(result.Diagnostics,
            d => d.Message.Contains("shadowedExporter", StringComparison.Ordinal)
                && d.Message.Contains("has collisions", StringComparison.Ordinal));
    }
 
    #endregion
 
    #region Callback Parameter Type Resolution Tests
 
    [Fact]
    public void ScanAssembly_MultiParamCallbackTypes_AreResolved()
    {
        // Regression test: callback parameter types must be resolved (not left as Unknown)
        // when the types are exported. Previously only param.Type was resolved but not
        // param.CallbackParameters[i].Type.
        var testAssembly = typeof(AtsCapabilityScannerTests).Assembly;
        var hostingAssembly = typeof(DistributedApplication).Assembly;
 
        var result = AtsCapabilityScanner.ScanAssemblies([hostingAssembly, testAssembly]);
 
        // Find the testMultiParamHandleCallback capability
        var capability = result.Capabilities
            .FirstOrDefault(c => c.CapabilityId.EndsWith("/testMultiParamHandleCallback", StringComparison.Ordinal));
 
        Assert.NotNull(capability);
 
        var callbackParam = Assert.Single(capability.Parameters, p => p.IsCallback);
        Assert.NotNull(callbackParam.CallbackParameters);
        Assert.Equal(2, callbackParam.CallbackParameters.Count);
 
        // Both callback parameter types should be resolved to Handle (not Unknown)
        foreach (var cbParam in callbackParam.CallbackParameters)
        {
            Assert.NotNull(cbParam.Type);
            Assert.NotEqual(AtsTypeCategory.Unknown, cbParam.Type.Category);
        }
    }
 
    [Fact]
    public void ScanAssemblies_AssemblyLevelExportedTypes_AreResolvedAcrossScanOrder()
    {
        Assert.Null(AtsCapabilityScanner.MapToAtsTypeId(typeof(AssemblyLevelExportedTestType)));
 
        var capabilityAssembly = CreateAssemblyLevelExportCapabilityAssembly(typeof(AssemblyLevelExportedTestType));
        var exportAssembly = CreateAssemblyLevelExportAssembly(typeof(AssemblyLevelExportedTestType));
 
        var result = AtsCapabilityScanner.ScanAssemblies([capabilityAssembly, exportAssembly]);
 
        var capability = Assert.Single(result.Capabilities,
            c => c.CapabilityId.EndsWith("/usesAssemblyExportedType", StringComparison.Ordinal));
        var parameter = Assert.Single(capability.Parameters);
 
        Assert.NotNull(parameter.Type);
        Assert.Equal(AtsTypeCategory.Handle, parameter.Type.Category);
        Assert.Equal(AtsTypeMapping.DeriveTypeId(typeof(AssemblyLevelExportedTestType)), parameter.Type.TypeId);
        Assert.Equal(
            AtsTypeMapping.DeriveTypeId(typeof(AssemblyLevelExportedTestType)),
            AtsCapabilityScanner.MapToAtsTypeId(typeof(AssemblyLevelExportedTestType)));
    }
 
    [Fact]
    public void ScanAssembly_YarpWithConfiguration_UsesBackgroundThreadOptIn()
    {
        var yarpAssembly = typeof(global::Aspire.Hosting.Yarp.YarpResource).Assembly;
 
        var result = AtsCapabilityScanner.ScanAssembly(yarpAssembly);
 
        var capability = Assert.Single(result.Capabilities,
            c => c.CapabilityId.EndsWith("/withConfiguration", StringComparison.Ordinal));
        var withConfigurationMethod = Assert.Single(result.Methods,
            m => m.Key.EndsWith("/withConfiguration", StringComparison.Ordinal)).Value;
 
        Assert.True(capability.RunSyncOnBackgroundThread);
        Assert.Equal(typeof(IResourceBuilder<global::Aspire.Hosting.Yarp.YarpResource>), withConfigurationMethod.ReturnType);
 
        var parameters = withConfigurationMethod.GetParameters();
        Assert.Equal(2, parameters.Length);
        Assert.Equal(typeof(IResourceBuilder<global::Aspire.Hosting.Yarp.YarpResource>), parameters[0].ParameterType);
        Assert.Equal(typeof(Action<global::Aspire.Hosting.IYarpConfigurationBuilder>), parameters[1].ParameterType);
    }
 
    [Fact]
    public void ScanAssembly_ClassLevelBackgroundThreadOptIn_AppliesToExportedMethods()
    {
        var result = AtsCapabilityScanner.ScanAssembly(typeof(AtsCapabilityScannerTests).Assembly);
 
        var capability = Assert.Single(result.Capabilities,
            c => c.CapabilityId.EndsWith("/classLevelBackgroundThreadProbe", StringComparison.Ordinal));
 
        Assert.True(capability.RunSyncOnBackgroundThread);
    }
 
    [Fact]
    public void ScanAssembly_HostingAssembly_CallbackBearingInteractionPrompts_UseBackgroundThreadOptIn()
    {
        // These prompts accept option/handle types that carry user callbacks (progress work, dynamic input
        // loading, validation). The callbacks re-enter the remote host over the same JSON-RPC connection, so the
        // synchronous invocation path has to run on a background thread or the RPC loop deadlocks waiting on itself.
        // AspireExportAnalyzer only detects delegates passed *directly* as parameters, so it cannot enforce the
        // opt-in when the delegate is reached through an options object or builder handle. This test is the guard.
        var hostingAssembly = typeof(DistributedApplication).Assembly;
 
        var result = AtsCapabilityScanner.ScanAssembly(hostingAssembly);
 
        string[] expectedCapabilityIds =
        [
            "Aspire.Hosting/promptProgress",
            "Aspire.Hosting/promptInput",
            "Aspire.Hosting/promptInputs"
        ];
 
        foreach (var expectedCapabilityId in expectedCapabilityIds)
        {
            var capability = Assert.Single(result.Capabilities, c => c.CapabilityId == expectedCapabilityId);
 
            Assert.True(
                capability.RunSyncOnBackgroundThread,
                $"'{expectedCapabilityId}' invokes polyglot callbacks and must set RunSyncOnBackgroundThread = true.");
        }
    }
 
    #endregion
 
    #region Exported Value Tests
 
    [Fact]
    public void ScanAssembly_MutableDictionaryExportedValue_IsSkipped()
    {
        var result = AtsCapabilityScanner.ScanAssembly(typeof(AtsCapabilityScannerTests).Assembly);
 
        Assert.DoesNotContain(result.ExportedValues, value =>
            string.Join(".", value.PathSegments) == "InvalidValues.InvalidExportedValues.MutableMetadata");
        Assert.DoesNotContain(result.ExportedValues, value =>
            string.Join(".", value.PathSegments) == "InvalidValues.InvalidExportedValues.DtoWithMutableList");
        Assert.Contains(result.Diagnostics, diagnostic =>
            diagnostic.Severity == AtsDiagnosticSeverity.Warning
            && diagnostic.Message.Contains("copied shapes", StringComparison.Ordinal)
            && diagnostic.Location == "Aspire.Hosting.RemoteHost.Tests.AtsCapabilityScannerTests+InvalidExportedValues.MutableMetadata");
        Assert.Contains(result.Diagnostics, diagnostic =>
            diagnostic.Severity == AtsDiagnosticSeverity.Warning
            && diagnostic.Message.Contains("copied shapes", StringComparison.Ordinal)
            && diagnostic.Location == "Aspire.Hosting.RemoteHost.Tests.AtsCapabilityScannerTests+InvalidExportedValues.DtoWithMutableList");
    }
 
    [Fact]
    public void ScanAssembly_GetOnlyMutableCollectionDtoProperties_EmitWarnings()
    {
        var result = AtsCapabilityScanner.ScanAssembly(typeof(AtsCapabilityScannerTests).Assembly);
 
        Assert.Contains(result.Diagnostics, diagnostic =>
            diagnostic.Severity == AtsDiagnosticSeverity.Warning
            && diagnostic.Message.Contains("Add an init accessor", StringComparison.Ordinal)
            && diagnostic.Location == $"{typeof(GetOnlyCollectionDto).FullName}.{nameof(GetOnlyCollectionDto.Items)}");
        Assert.Contains(result.Diagnostics, diagnostic =>
            diagnostic.Severity == AtsDiagnosticSeverity.Warning
            && diagnostic.Message.Contains("Add an init accessor", StringComparison.Ordinal)
            && diagnostic.Location == $"{typeof(GetOnlyCollectionDto).FullName}.{nameof(GetOnlyCollectionDto.Metadata)}");
    }
 
    [Fact]
    public void ScanAssembly_InitDtoProperties_AreOptionalUnlessRequired()
    {
        var result = AtsCapabilityScanner.ScanAssembly(typeof(AtsCapabilityScannerTests).Assembly);
        var dto = Assert.Single(result.DtoTypes, d => d.TypeId == AtsTypeMapping.DeriveTypeId(typeof(InitPropertiesDto)));
 
        Assert.True(Assert.Single(dto.Properties, p => p.Name == nameof(InitPropertiesDto.DisplayName)).IsOptional);
        Assert.True(Assert.Single(dto.Properties, p => p.Name == nameof(InitPropertiesDto.Items)).IsOptional);
        Assert.True(Assert.Single(dto.Properties, p => p.Name == nameof(InitPropertiesDto.Metadata)).IsOptional);
        Assert.False(Assert.Single(dto.Properties, p => p.Name == nameof(InitPropertiesDto.RequiredDisplayName)).IsOptional);
        Assert.False(Assert.Single(dto.Properties, p => p.Name == nameof(InitPropertiesDto.RequiredItems)).IsOptional);
    }
 
    [Fact]
    public void ScanAssembly_ExportedDtoValueWithIgnoredMutableProperty_IsIncluded()
    {
        var result = AtsCapabilityScanner.ScanAssembly(typeof(AtsCapabilityScannerTests).Assembly);
 
        var exportedValue = Assert.Single(result.ExportedValues,
            value => string.Join(".", value.PathSegments) == "IgnoredPropertyValues.IgnoredPropertyExportedValues.Value");
        var valueObject = Assert.IsType<JsonObject>(exportedValue.Value);
        Assert.True(valueObject.ContainsKey(nameof(ExportedDtoWithIgnoredMutableProperty.Name)));
        Assert.False(valueObject.ContainsKey(nameof(ExportedDtoWithIgnoredMutableProperty.Items)));
    }
 
    [Fact]
    public void ScanAssembly_InvalidExportedValuePath_EmitsWarningAndSkipsValue()
    {
        var result = AtsCapabilityScanner.ScanAssembly(typeof(AtsCapabilityScannerTests).Assembly);
 
        Assert.DoesNotContain(result.ExportedValues, value =>
            string.Join(".", value.PathSegments).Contains("Invalid-Values", StringComparison.Ordinal));
        Assert.DoesNotContain(result.ExportedValues, value =>
            string.Join(".", value.PathSegments).Contains("123Name", StringComparison.Ordinal));
        Assert.Contains(result.Diagnostics, diagnostic =>
            diagnostic.Severity == AtsDiagnosticSeverity.Warning
            && diagnostic.Message.Contains("path segment 'Invalid-Values'", StringComparison.Ordinal)
            && diagnostic.Location == "Aspire.Hosting.RemoteHost.Tests.AtsCapabilityScannerTests+InvalidExportedValuePaths.BadCatalog");
        Assert.Contains(result.Diagnostics, diagnostic =>
            diagnostic.Severity == AtsDiagnosticSeverity.Warning
            && diagnostic.Message.Contains("path segment '123Name'", StringComparison.Ordinal)
            && diagnostic.Location == "Aspire.Hosting.RemoteHost.Tests.AtsCapabilityScannerTests+InvalidExportedValuePaths.BadName");
    }
 
    [Fact]
    public void ScanAssembly_DuplicateExportedValuePath_EmitsWarningAndSkipsLaterValue()
    {
        var result = AtsCapabilityScanner.ScanAssembly(typeof(AtsCapabilityScannerTests).Assembly);
 
        Assert.Single(result.ExportedValues,
            value => string.Join(".", value.PathSegments) == "DuplicateValues.DuplicateExportedValues.Shared");
        Assert.Contains(result.Diagnostics, diagnostic =>
            diagnostic.Severity == AtsDiagnosticSeverity.Warning
            && diagnostic.Message.Contains(
                "Duplicate exported value path 'DuplicateValues.DuplicateExportedValues.Shared'",
                StringComparison.Ordinal)
            && diagnostic.Location == "DuplicateValues.DuplicateExportedValues.Shared");
    }
 
    [Fact]
    public void ScanAssembly_PrefixConflictingExportedValuePath_EmitsWarningAndSkipsLaterValue()
    {
        var result = AtsCapabilityScanner.ScanAssembly(typeof(AtsCapabilityScannerTests).Assembly);
 
        Assert.DoesNotContain(result.ExportedValues, value =>
            string.Join(".", value.PathSegments) == "ConflictingValues.PrefixConflictingExportedValues.Node.Child");
        Assert.Contains(result.Diagnostics, diagnostic =>
            diagnostic.Severity == AtsDiagnosticSeverity.Warning
            && diagnostic.Message.Contains(
                "Conflicting exported value path 'ConflictingValues.PrefixConflictingExportedValues.Node.Child'",
                StringComparison.Ordinal)
            && diagnostic.Message.Contains(
                "Existing path 'ConflictingValues.PrefixConflictingExportedValues.Node'",
                StringComparison.Ordinal)
            && diagnostic.Location == "ConflictingValues.PrefixConflictingExportedValues.Node.Child");
    }
 
    [Fact]
    public void ScanAssembly_DescriptionFallback_PopulatesDocumentationSummaryWhenXmlDocsArePartial()
    {
        var result = AtsCapabilityScanner.ScanAssembly(typeof(AtsCapabilityScannerTests).Assembly);
 
        var capability = Assert.Single(result.Capabilities,
            c => c.CapabilityId.EndsWith("/descriptionFallback", StringComparison.Ordinal));
 
        Assert.Equal("Uses the description as fallback documentation.", capability.Description);
        Assert.Equal("Uses the description as fallback documentation.", capability.Documentation?.Summary);
        Assert.Equal("The fallback value.", Assert.Single(capability.Parameters).Documentation?.Summary);
    }
 
    #endregion
 
    #region Test Types
 
    private sealed class TestResource : Resource
    {
        public TestResource(string name) : base(name)
        {
        }
    }
 
    private sealed class ShadowedEnvironmentResource(string name) : Resource(name), IResourceWithEnvironment;
 
    private sealed class OtherEnvironmentResource(string name) : Resource(name), IResourceWithEnvironment;
 
    private enum TestUnionEnum
    {
        First,
        Second
    }
 
    [AspireDto]
    private sealed class NullableScalarDto
    {
        public string? NullableString { get; set; }
 
        public string RequiredString { get; set; } = "";
 
        public int? NullableNumber { get; set; }
 
        public int RequiredNumber { get; set; }
    }
 
    [AspireExport(ExposeProperties = true)]
    private class BaseExportedProperties
    {
        public string Name { get; } = "";
    }
 
    [AspireExport(ExposeProperties = true)]
    private sealed class DerivedExportedProperties : BaseExportedProperties
    {
        public string Framework { get; } = "";
    }
 
    [AspireExport(ExposeProperties = true)]
    private sealed class InitOnlyExportedProperties
    {
        public required string Name { get; init; }
 
        public string? Description { get; init; }
 
        public string MutableLabel { get; set; } = "";
    }
 
    public sealed class AssemblyLevelExportedTestType
    {
    }
 
    private static class TestExports
    {
        [AspireExport]
        public static void TestEnumerableParameter(IDistributedApplicationBuilder builder, IEnumerable<string> items)
        {
            _ = builder;
            _ = items;
        }
 
        [AspireExport]
        public static IEnumerable<string> TestEnumerableReturn(IDistributedApplicationBuilder builder)
        {
            _ = builder;
            return [];
        }
 
        [AspireExport]
        public static IResourceBuilder<TestResource> TestMultiParamHandleCallback(
            IResourceBuilder<TestResource> builder,
            Func<ContainerResource, ProjectResource, Task> callback)
        {
            _ = callback;
            return builder;
        }
 
        [AspireExport]
        public static void TestUnionEnumParameter(IDistributedApplicationBuilder builder, [AspireUnion(typeof(TestUnionEnum), typeof(string))] object value)
        {
            _ = builder;
            _ = value;
        }
 
        /// <param name="value">The fallback value.</param>
        [AspireExport("descriptionFallback", Description = "Uses the description as fallback documentation.")]
        public static void DescriptionFallback(IDistributedApplicationBuilder builder, string value)
        {
            _ = builder;
            _ = value;
        }
 
        [AspireExport("shadowedExporter")]
        public static IResourceBuilder<T> ShadowedExporter<T>(IResourceBuilder<T> builder)
            where T : IResourceWithEnvironment
        {
            return builder;
        }
 
        [AspireExport("specificShadowedExporter", MethodName = "shadowedExporter")]
        public static IResourceBuilder<ShadowedEnvironmentResource> SpecificShadowedExporter(IResourceBuilder<ShadowedEnvironmentResource> builder)
        {
            return builder;
        }
 
        [AspireExport("otherEnvironmentProbe")]
        public static IResourceBuilder<OtherEnvironmentResource> OtherEnvironmentProbe(IResourceBuilder<OtherEnvironmentResource> builder)
        {
            return builder;
        }
    }
 
    private static class InvalidExportedValues
    {
        [AspireValue("InvalidValues")]
        public static Dictionary<string, string> MutableMetadata { get; } = [];
 
        [AspireValue("InvalidValues")]
        public static InvalidExportedDto DtoWithMutableList { get; } = new();
    }
 
    [AspireDto]
    private sealed class InvalidExportedDto
    {
        public List<string> Items { get; set; } = [];
    }
 
    [AspireDto]
    private sealed class GetOnlyCollectionDto
    {
        public List<string> Items { get; } = [];
 
        public Dictionary<string, string> Metadata { get; } = [];
    }
 
    [AspireDto]
    private sealed class InitPropertiesDto
    {
        public string DisplayName { get; init; } = "";
 
        public List<string> Items { get; init; } = [];
 
        public Dictionary<string, string> Metadata { get; init; } = [];
 
        public required string RequiredDisplayName { get; init; }
 
        public required List<string> RequiredItems { get; init; }
    }
 
    private static class IgnoredPropertyExportedValues
    {
        [AspireValue("IgnoredPropertyValues")]
        public static ExportedDtoWithIgnoredMutableProperty Value { get; } = new()
        {
            Name = "valid",
            Items = ["ignored"]
        };
    }
 
    [AspireDto]
    private sealed class ExportedDtoWithIgnoredMutableProperty
    {
        public string Name { get; init; } = "";
 
        [AspireExportIgnore]
        public List<string> Items { get; init; } = [];
    }
 
    private static class InvalidExportedValuePaths
    {
        [AspireValue("Invalid-Values")]
        public static string BadCatalog { get; } = "bad";
 
        [AspireValue("InvalidValues", Name = "123Name")]
        public static string BadName { get; } = "bad";
    }
 
    private static class DuplicateExportedValues
    {
        [AspireValue("DuplicateValues", Name = "Shared")]
        public static string FirstShared { get; } = "first";
 
        [AspireValue("DuplicateValues", Name = "Shared")]
        public static string SecondShared { get; } = "second";
    }
 
    private static class PrefixConflictingExportedValues
    {
        [AspireValue("ConflictingValues", Name = "Node")]
        public static string NodeValue { get; } = "root";
 
        public static class Node
        {
            [AspireValue("ConflictingValues", Name = "Child")]
            public static string ChildValue { get; } = "child";
        }
    }
 
    private static Assembly CreateAssemblyLevelExportCapabilityAssembly(Type parameterType)
    {
        var assemblyName = new AssemblyName($"AssemblyLevelExportCapability_{Guid.NewGuid():N}");
        var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
        var moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName.Name!);
        var exportsTypeBuilder = moduleBuilder.DefineType(
            "Generated.AssemblyLevelExportedTypeExports",
            TypeAttributes.Public | TypeAttributes.Abstract | TypeAttributes.Sealed);
        var methodBuilder = exportsTypeBuilder.DefineMethod(
            "UsesAssemblyExportedType",
            MethodAttributes.Public | MethodAttributes.Static,
            typeof(void),
            [typeof(IDistributedApplicationBuilder), parameterType]);
        methodBuilder.DefineParameter(1, ParameterAttributes.None, "builder");
        methodBuilder.DefineParameter(2, ParameterAttributes.None, "value");
        methodBuilder.SetCustomAttribute(
            new CustomAttributeBuilder(
                typeof(AspireExportAttribute).GetConstructor([typeof(string)])!,
                ["usesAssemblyExportedType"]));
        methodBuilder.GetILGenerator().Emit(OpCodes.Ret);
 
        _ = exportsTypeBuilder.CreateType();
 
        return assemblyBuilder;
    }
 
    private static Assembly CreateAssemblyLevelExportAssembly(Type exportedType)
    {
        var assemblyName = new AssemblyName($"AssemblyLevelExport_{Guid.NewGuid():N}");
        var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
        _ = assemblyBuilder.DefineDynamicModule(assemblyName.Name!);
        assemblyBuilder.SetCustomAttribute(
            new CustomAttributeBuilder(
                typeof(AspireExportAttribute).GetConstructor([typeof(Type)])!,
                [exportedType]));
 
        return assemblyBuilder;
    }
 
    [AspireExport(RunSyncOnBackgroundThread = true)]
    private static class ClassLevelBackgroundThreadExports
    {
        [AspireExport("classLevelBackgroundThreadProbe")]
        public static void Probe(IDistributedApplicationBuilder builder)
        {
            _ = builder;
        }
    }
 
    #endregion
 
    #region XML Documentation Extraction Tests
 
    [Fact]
    public void GetXmlDocSummary_ReturnsNull_WhenDocIsNull()
    {
        var result = AtsCapabilityScanner.GetXmlDocSummary(null, "T:Some.Type");
 
        Assert.Null(result);
    }
 
    [Fact]
    public void GetXmlDocSummary_ReturnsNull_WhenMemberNotFound()
    {
        var doc = System.Xml.Linq.XDocument.Parse("""
            <?xml version="1.0"?>
            <doc>
              <members>
                <member name="T:Some.OtherType">
                  <summary>Other type.</summary>
                </member>
              </members>
            </doc>
            """);
 
        var result = AtsCapabilityScanner.GetXmlDocSummary(doc, "T:Some.Type");
 
        Assert.Null(result);
    }
 
    [Fact]
    public void GetXmlDocSummary_ExtractsTypeSummary()
    {
        var doc = System.Xml.Linq.XDocument.Parse("""
            <?xml version="1.0"?>
            <doc>
              <members>
                <member name="T:Some.MyDto">
                  <summary>Options for creating a builder.</summary>
                </member>
              </members>
            </doc>
            """);
 
        var result = AtsCapabilityScanner.GetXmlDocSummary(doc, "T:Some.MyDto");
 
        Assert.Equal("Options for creating a builder.", result);
    }
 
    [Fact]
    public void GetXmlDocSummary_ExtractsPropertySummary()
    {
        var doc = System.Xml.Linq.XDocument.Parse("""
            <?xml version="1.0"?>
            <doc>
              <members>
                <member name="P:Some.MyDto.Name">
                  <summary>The resource name.</summary>
                </member>
              </members>
            </doc>
            """);
 
        var result = AtsCapabilityScanner.GetXmlDocSummary(doc, "P:Some.MyDto.Name");
 
        Assert.Equal("The resource name.", result);
    }
 
    [Fact]
    public void GetXmlDocSummary_NormalizesMultilineWhitespace()
    {
        var doc = System.Xml.Linq.XDocument.Parse("""
            <?xml version="1.0"?>
            <doc>
              <members>
                <member name="T:Some.MyDto">
                  <summary>
                    Options for creating
                    a distributed application builder.
                  </summary>
                </member>
              </members>
            </doc>
            """);
 
        var result = AtsCapabilityScanner.GetXmlDocSummary(doc, "T:Some.MyDto");
 
        Assert.Equal("Options for creating a distributed application builder.", result);
    }
 
    [Fact]
    public void GetXmlDocSummary_ReturnsNull_WhenSummaryIsEmpty()
    {
        var doc = System.Xml.Linq.XDocument.Parse("""
            <?xml version="1.0"?>
            <doc>
              <members>
                <member name="T:Some.MyDto">
                  <summary>   </summary>
                </member>
              </members>
            </doc>
            """);
 
        var result = AtsCapabilityScanner.GetXmlDocSummary(doc, "T:Some.MyDto");
 
        Assert.Null(result);
    }
 
    [Fact]
    public void LoadXmlDocumentation_ReturnsCachedResult()
    {
        // Loading for the same assembly twice should return the same object
        var assembly = typeof(DistributedApplication).Assembly;
        var first = AtsCapabilityScanner.LoadXmlDocumentation(assembly);
        var second = AtsCapabilityScanner.LoadXmlDocumentation(assembly);
 
        Assert.NotNull(first);
        Assert.Same(first, second);
    }
 
    #endregion
}