Enable output marshaling in .NET (#8316)

This commit is contained in:
Ian Wahbe 2021-11-12 14:58:34 -08:00 committed by GitHub
parent 5dd7851293
commit 164a2ec818
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 299 additions and 64 deletions

View file

@ -7,6 +7,9 @@
- [schema] Add IsOverlay option to disable codegen for particular types
[#8338](https://github.com/pulumi/pulumi/pull/8338)
- [sdk/dotnet] - Marshal output values.
[#8316](https://github.com/pulumi/pulumi/pull/8316)
### Bug Fixes
- [engine] - Compute dependents correctly during targeted deletes.

View file

@ -1,6 +1,7 @@
// Copyright 2016-2019, Pulumi Corporation
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace Pulumi.Tests

View file

@ -19,7 +19,7 @@ namespace Pulumi.Tests.Serialization
Fields =
{
{ Constants.SpecialSigKey, new Value { StringValue = Constants.SpecialSecretSig } },
{ Constants.SecretValueName, value },
{ Constants.ValueName, value },
}
}
};

View file

@ -0,0 +1,141 @@
// Copyright 2016-2021, Pulumi Corporation
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Pulumi.Serialization;
using Xunit;
namespace Pulumi.Tests.Serialization
{
public class MarshalOutputTests : PulumiTest
{
public static IEnumerable<object[]> BasicSerializeData =>
from value in new object?[]
{
null,
0.0,
1.0,
"",
"hi",
ImmutableDictionary<string, object?>.Empty,
ImmutableArray<object?>.Empty,
}
from deps in new[] { Array.Empty<string>(), new[] { "fakeURN1", "fakeURN2" } }
from isKnown in new[] { true, false }
from isSecret in new[] { true, false }
select new object[] { value, deps, isKnown, isSecret };
[Theory]
[MemberData(nameof(BasicSerializeData))]
public static Task TestBasicSerialize(object? value, string[] deps, bool isKnown, bool isSecret) => RunInNormal(async () =>
{
var resources = ImmutableHashSet.CreateRange<Resource>(deps.Select(d => new DependencyResource(d)));
var data = OutputData.Create(resources, value, isKnown, isSecret);
var input = new Output<object?>(Task.FromResult(data));
var expected = isKnown && !isSecret && deps.Length == 0
? value
: CreateOutputValue(value, isKnown, isSecret, deps);
var s = new Serializer(excessiveDebugOutput: false);
var actual = await s.SerializeAsync("", input, keepResources: true, keepOutputValues: true);
Assert.Equal(expected, actual);
});
public sealed class FooArgs : ResourceArgs
{
[Input("foo")]
public Input<string>? Foo { get; set; }
}
public sealed class BarArgs : ResourceArgs
{
[Input("foo")]
public Input<FooArgs>? Foo { get; set; }
}
public static IEnumerable<object[]> SerializeData() => new object[][]
{
new object[]
{
new FooArgs { Foo = "hello" },
ImmutableDictionary<string, object>.Empty.Add("foo", "hello")
},
new object[]
{
new FooArgs { Foo = Output.Create("hello") },
ImmutableDictionary<string, object>.Empty.Add("foo", "hello")
},
new object[]
{
new FooArgs { Foo = Output.CreateSecret("hello") },
ImmutableDictionary<string, object>.Empty.Add("foo", CreateOutputValue("hello", isSecret: true))
},
new object[]
{
new List<Input<string>> { "hello" },
ImmutableArray<object>.Empty.Add("hello")
},
new object[]
{
new List<Input<string>> { Output.Create("hello") },
ImmutableArray<object>.Empty.Add("hello")
},
new object[]
{
new List<Input<string>> { Output.CreateSecret("hello") },
ImmutableArray<object>.Empty.Add(CreateOutputValue("hello", isSecret: true))
},
new object[]
{
new Dictionary<string, Input<string>> { { "foo", "hello" } },
ImmutableDictionary<string, object>.Empty.Add("foo", "hello")
},
new object[]
{
new Dictionary<string, Input<string>> { { "foo", Output.Create("hello") } },
ImmutableDictionary<string, object>.Empty.Add("foo", "hello")
},
new object[]
{
new Dictionary<string, Input<string>> { { "foo", Output.CreateSecret("hello") } },
ImmutableDictionary<string, object>.Empty.Add("foo", CreateOutputValue("hello", isSecret: true))
},
new object[]
{
new BarArgs { Foo = new FooArgs { Foo = "hello" } },
ImmutableDictionary<string, object>.Empty.Add("foo",
ImmutableDictionary<string, object>.Empty.Add("foo", "hello"))
},
new object[]
{
new BarArgs { Foo = new FooArgs { Foo = Output.Create("hello") } },
ImmutableDictionary<string, object>.Empty.Add("foo",
ImmutableDictionary<string, object>.Empty.Add("foo", "hello"))
},
};
[Theory]
[MemberData(nameof(SerializeData))]
public static Task TestSerialize(object input, object expected) => RunInNormal(async () =>
{
var s = new Serializer(excessiveDebugOutput: false);
var actual = await s.SerializeAsync("", input, keepResources: true, keepOutputValues: true);
Assert.Equal(expected, actual);
});
private static ImmutableDictionary<string, object?> CreateOutputValue(
object? value, bool isKnown = true, bool isSecret = false, params string[] deps)
{
var b = ImmutableDictionary.CreateBuilder<string, object?>();
b.Add(Constants.SpecialSigKey, Constants.SpecialOutputValueSig);
if (isKnown) b.Add(Constants.ValueName, value);
if (isSecret) b.Add(Constants.SecretName, isSecret);
if (deps.Length > 0) b.Add(Constants.DependenciesName, deps.ToImmutableArray());
return b.ToImmutableDictionary();
}
}
}

View file

@ -192,7 +192,7 @@ namespace Pulumi
{
if (!this._featureSupport.ContainsKey(feature))
{
var request = new SupportsFeatureRequest {Id = feature };
var request = new SupportsFeatureRequest { Id = feature };
var response = await this.Monitor.SupportsFeatureAsync(request).ConfigureAwait(false);
this._featureSupport[feature] = response.HasSupport;
}
@ -203,5 +203,16 @@ namespace Pulumi
{
return MonitorSupportsFeature("resourceReferences");
}
/// <summary>
/// Check if the monitor supports the "outputValues" feature.
/// </summary>
internal Task<bool> MonitorSupportsOutputValues()
{
return MonitorSupportsFeature("outputValues");
}
// Because the secrets feature predates the Pulumi .NET SDK, we assume
// that the monitor supports secrets.
}
}

View file

@ -53,8 +53,10 @@ namespace Pulumi
}
var (serialized, argDependencies) = await SerializeFilteredPropertiesAsync(
$"call:{token}",
argsDict, _ => true, keepResources: true).ConfigureAwait(false);
$"call:{token}",
argsDict, _ => true,
keepResources: true,
keepOutputValues: await MonitorSupportsOutputValues().ConfigureAwait(false)).ConfigureAwait(false);
Log.Debug($"Call RPC prepared: token={token}" +
(_excessiveDebugOutput ? $", obj={serialized}" : ""));

View file

@ -148,7 +148,8 @@ namespace Pulumi
label: $"invoke:{token}",
args: argsDict,
acceptKey: key => true,
keepResources: keepResources
keepResources: keepResources,
keepOutputValues: false
).ConfigureAwait(false);
}

View file

@ -33,7 +33,8 @@ namespace Pulumi
await SerializeResourcePropertiesAsync(
label,
dictionary,
await this.MonitorSupportsResourceReferences().ConfigureAwait(false)).ConfigureAwait(false);
await this.MonitorSupportsResourceReferences().ConfigureAwait(false),
keepOutputValues: remote && await MonitorSupportsOutputValues().ConfigureAwait(false)).ConfigureAwait(false);
LogExcessive($"Serialized properties: t={type}, name={name}, custom={custom}, remote={remote}");
// Wait for the parent to complete.
@ -123,7 +124,7 @@ namespace Pulumi
private static Task<ImmutableArray<Resource>> GatherExplicitDependenciesAsync(InputList<Resource> resources)
=> resources.ToOutput().GetValueAsync(whenUnknown: ImmutableArray<Resource>.Empty);
private static async Task<HashSet<string>> GetAllTransitivelyReferencedResourceUrnsAsync(
internal static async Task<HashSet<string>> GetAllTransitivelyReferencedResourceUrnsAsync(
HashSet<Resource> resources)
{
// Go through 'resources', but transitively walk through **Component** resources, collecting any
@ -152,8 +153,10 @@ namespace Pulumi
// * Comp3 and Cust5 because Comp3 is a child of a remote component resource
var transitivelyReachableResources = GetTransitivelyReferencedChildResourcesOfComponentResources(resources);
var transitivelyReachableCustomResources = transitivelyReachableResources.Where(res => {
switch (res) {
var transitivelyReachableCustomResources = transitivelyReachableResources.Where(res =>
{
switch (res)
{
case CustomResource _: return true;
case ComponentResource component: return component.remote;
default: return false; // Unreachable

View file

@ -28,8 +28,8 @@ namespace Pulumi
var urn = await resource.Urn.GetValueAsync(whenUnknown: default!).ConfigureAwait(false);
var props = await outputs.GetValueAsync(whenUnknown: default!).ConfigureAwait(false);
var serialized = await SerializeAllPropertiesAsync(
opLabel, props, await MonitorSupportsResourceReferences().ConfigureAwait(false)).ConfigureAwait(false);
var keepResources = await MonitorSupportsResourceReferences().ConfigureAwait(false);
var serialized = await SerializeAllPropertiesAsync(opLabel, props, keepResources).ConfigureAwait(false);
Log.Debug($"RegisterResourceOutputs RPC prepared: urn={urn}" +
(_excessiveDebugOutput ? $", outputs={JsonFormatter.Default.Format(serialized)}" : ""));

View file

@ -20,16 +20,20 @@ namespace Pulumi
/// to registerResource.
/// </summary>
private static Task<SerializationResult> SerializeResourcePropertiesAsync(
string label, IDictionary<string, object?> args, bool keepResources)
string label, IDictionary<string, object?> args, bool keepResources, bool keepOutputValues)
{
return SerializeFilteredPropertiesAsync(
label, args, key => key != Constants.IdPropertyName && key != Constants.UrnPropertyName, keepResources);
label, args,
key => key != Constants.IdPropertyName && key != Constants.UrnPropertyName,
keepResources, keepOutputValues: keepOutputValues);
}
private static async Task<Struct> SerializeAllPropertiesAsync(
string label, IDictionary<string, object?> args, bool keepResources)
string label, IDictionary<string, object?> args, bool keepResources, bool keepOutputValues = false)
{
var result = await SerializeFilteredPropertiesAsync(label, args, _ => true, keepResources).ConfigureAwait(false);
var result = await SerializeFilteredPropertiesAsync(
label, args, _ => true,
keepResources, keepOutputValues).ConfigureAwait(false);
return result.Serialized;
}
@ -38,10 +42,20 @@ namespace Pulumi
/// awaiting all interior promises for properties with keys that match the provided filter,
/// creating a reasonable POCO object that can be remoted over to registerResource.
/// </summary>
///
/// <param name="label">label</param>
/// <param name="args">args</param>
/// <param name="acceptKey">acceptKey</param>
/// <param name="keepResources">keepResources</param>
/// <param name="keepOutputValues">
/// Specifies if we should marshal output values. It is the callers
/// responsibility to ensure that the monitor supports the OutputValues
/// feature.
/// </param>
private static async Task<SerializationResult> SerializeFilteredPropertiesAsync(
string label, IDictionary<string, object?> args, Predicate<string> acceptKey, bool keepResources)
string label, IDictionary<string, object?> args, Predicate<string> acceptKey, bool keepResources, bool keepOutputValues)
{
var result = await SerializeFilteredPropertiesRawAsync(label, args, acceptKey, keepResources);
var result = await SerializeFilteredPropertiesRawAsync(label, args, acceptKey, keepResources, keepOutputValues);
return result.ToSerializationResult();
}
@ -50,7 +64,7 @@ namespace Pulumi
/// last step of encoding the value into a Protobuf form.
/// </summary>
private static async Task<RawSerializationResult> SerializeFilteredPropertiesRawAsync(
string label, IDictionary<string, object?> args, Predicate<string> acceptKey, bool keepResources)
string label, IDictionary<string, object?> args, Predicate<string> acceptKey, bool keepResources, bool keepOutputValues)
{
var propertyToDependentResources = ImmutableDictionary.CreateBuilder<string, HashSet<Resource>>();
var result = ImmutableDictionary.CreateBuilder<string, object>();
@ -61,7 +75,7 @@ namespace Pulumi
{
// We treat properties with null values as if they do not exist.
var serializer = new Serializer(_excessiveDebugOutput);
var v = await serializer.SerializeAsync($"{label}.{key}", val, keepResources).ConfigureAwait(false);
var v = await serializer.SerializeAsync($"{label}.{key}", val, keepResources, keepOutputValues).ConfigureAwait(false);
if (v != null)
{
result[key] = v;

View file

@ -34,7 +34,14 @@ namespace Pulumi.Serialization
/// </summary>
public const string SpecialResourceSig = "5cf8f73096256a8f31e491e813e4eb8e";
public const string SecretValueName = "value";
/// <summary>
/// SpecialOutputValueSig is a randomly assigned hash used to identify outputs in maps. See sdk/go/common/resource/properties.go.
/// </summary>
public const string SpecialOutputValueSig = "d0e6a833031e9bbcd3f4e8bde6ca49a4";
public const string SecretName = "secret";
public const string ValueName = "value";
public const string DependenciesName = "dependencies";
public const string AssetTextName = "text";
public const string ArchiveAssetsName = "assets";

View file

@ -100,7 +100,7 @@ namespace Pulumi.Serialization
});
public static OutputData<object?> Deserialize(Value value)
=> DeserializeCore(value,
=> DeserializeCore(value,
v => v.KindCase switch
{
Value.KindOneofCase.NumberValue => DeserializerDouble(v),
@ -120,7 +120,7 @@ namespace Pulumi.Serialization
while (IsSpecialStruct(value, out var sig) &&
sig == Constants.SpecialSecretSig)
{
if (!value.StructValue.Fields.TryGetValue(Constants.SecretValueName, out var secretValue))
if (!value.StructValue.Fields.TryGetValue(Constants.ValueName, out var secretValue))
throw new InvalidOperationException("Secrets must have a field called 'value'");
isSecret = true;
@ -222,7 +222,8 @@ namespace Pulumi.Serialization
throw new InvalidOperationException("Value was marked as a Resource, but did not conform to required shape.");
}
if (!TryGetStringValue(value.StructValue.Fields, Constants.ResourceVersionName, out var version)) {
if (!TryGetStringValue(value.StructValue.Fields, Constants.ResourceVersionName, out var version))
{
version = "";
}
@ -231,7 +232,8 @@ namespace Pulumi.Serialization
var qualifiedTypeParts = qualifiedType.Split('$');
var type = qualifiedTypeParts[^1];
if (ResourcePackages.TryConstruct(type, version, urn, out resource)) {
if (ResourcePackages.TryConstruct(type, version, urn, out resource))
{
return true;
}

View file

@ -1,4 +1,4 @@
// Copyright 2016-2019, Pulumi Corporation
// Copyright 2016-2021, Pulumi Corporation
using System;
using System.Collections;
@ -21,7 +21,7 @@ namespace Pulumi.Serialization
public Serializer(bool excessiveDebugOutput)
{
this.DependentResources = new HashSet<Resource>();
DependentResources = new HashSet<Resource>();
_excessiveDebugOutput = excessiveDebugOutput;
}
@ -39,7 +39,7 @@ namespace Pulumi.Serialization
/// <item><see cref="Asset"/>s</item>
/// <item><see cref="Archive"/>s</item>
/// <item><see cref="Resource"/>s</item>
/// <item><see cref="ResourceArgs"/>s</item>
/// <item><see cref="ResourceArgs"/></item>
/// <item><see cref="JsonElement"/></item>
/// </list>
/// Additionally, other more complex objects can be serialized as long as they are built
@ -67,7 +67,7 @@ namespace Pulumi.Serialization
/// </list>
/// No other result type are allowed to be returned.
/// </summary>
public async Task<object?> SerializeAsync(string ctx, object? prop, bool keepResources)
public async Task<object?> SerializeAsync(string ctx, object? prop, bool keepResources, bool keepOutputValues = false)
{
// IMPORTANT:
// IMPORTANT: Keep this in sync with serializesPropertiesSync in invoke.ts
@ -87,10 +87,15 @@ namespace Pulumi.Serialization
}
if (prop is InputArgs args)
return await SerializeInputArgsAsync(ctx, args, keepResources).ConfigureAwait(false);
{
return await SerializeInputArgsAsync(ctx, args, keepResources, keepOutputValues).ConfigureAwait(false);
}
if (prop is AssetOrArchive assetOrArchive)
{
// There's no need to pass keepOutputValues when serializing assets or archives.
return await SerializeAssetOrArchiveAsync(ctx, assetOrArchive, keepResources).ConfigureAwait(false);
}
if (prop is Task)
{
@ -105,7 +110,7 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output:
Log.Debug($"Serialize property[{ctx}]: Recursing into IInput");
}
return await SerializeAsync(ctx, input.ToOutput(), keepResources).ConfigureAwait(false);
return await SerializeAsync(ctx, input.ToOutput(), keepResources, keepOutputValues).ConfigureAwait(false);
}
if (prop is IUnion union)
@ -115,7 +120,7 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output:
Log.Debug($"Serialize property[{ctx}]: Recursing into IUnion");
}
return await SerializeAsync(ctx, union.Value, keepResources).ConfigureAwait(false);
return await SerializeAsync(ctx, union.Value, keepResources, keepOutputValues).ConfigureAwait(false);
}
if (prop is JsonElement element)
@ -134,9 +139,9 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output:
{
Log.Debug($"Serialize property[{ctx}]: Recursing into Output");
}
var data = await output.GetDataAsync().ConfigureAwait(false);
this.DependentResources.AddRange(data.Resources);
DependentResources.AddRange(data.Resources);
var propResources = new HashSet<Resource>(data.Resources);
// When serializing an Output, we will either serialize it as its resolved value or the "unknown value"
// sentinel. We will do the former for all outputs created directly by user code (such outputs always
@ -144,15 +149,56 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output:
var isKnown = data.IsKnown;
var isSecret = data.IsSecret;
var valueSerializer = new Serializer(_excessiveDebugOutput);
var value = await valueSerializer.SerializeAsync($"{ctx}.id", data.Value, keepResources, keepOutputValues: false).ConfigureAwait(false);
var promiseDeps = valueSerializer.DependentResources;
DependentResources.UnionWith(promiseDeps);
propResources.UnionWith(promiseDeps);
if (keepOutputValues)
{
if (isKnown && !isSecret && propResources.Count == 0)
{
return value;
}
var urnDeps = new HashSet<Resource>();
foreach (var resource in propResources)
{
var urnSerializer = new Serializer(_excessiveDebugOutput);
await urnSerializer.SerializeAsync($"{ctx} dependency", resource.Urn, keepResources, keepOutputValues: false).ConfigureAwait(false);
urnDeps.UnionWith(urnSerializer.DependentResources);
}
DependentResources.UnionWith(urnDeps);
propResources.UnionWith(urnDeps);
var dependencies = await Deployment.GetAllTransitivelyReferencedResourceUrnsAsync(propResources).ConfigureAwait(false);
var builder = ImmutableDictionary.CreateBuilder<string, object?>();
builder.Add(Constants.SpecialSigKey, Constants.SpecialOutputValueSig);
if (isKnown)
{
builder.Add(Constants.ValueName, value);
}
if (isSecret)
{
builder.Add(Constants.SecretName, isSecret);
}
if (dependencies.Count > 0)
{
builder.Add(Constants.DependenciesName,
dependencies.OrderBy(x => x, StringComparer.Ordinal).ToImmutableArray<object>());
}
return builder.ToImmutable();
}
if (!isKnown)
return Constants.UnknownValue;
var value = await SerializeAsync($"{ctx}.id", data.Value, keepResources).ConfigureAwait(false);
if (isSecret)
{
var builder = ImmutableDictionary.CreateBuilder<string, object?>();
builder.Add(Constants.SpecialSigKey, Constants.SpecialSecretSig);
builder.Add(Constants.SecretValueName, value);
builder.Add(Constants.ValueName, value);
return builder.ToImmutable();
}
@ -167,12 +213,12 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output:
Log.Debug($"Serialize property[{ctx}]: Encountered CustomResource");
}
this.DependentResources.Add(customResource);
DependentResources.Add(customResource);
var id = await SerializeAsync($"{ctx}.id", customResource.Id, keepResources).ConfigureAwait(false);
var id = await SerializeAsync($"{ctx}.id", customResource.Id, keepResources, keepOutputValues: false).ConfigureAwait(false);
if (keepResources)
{
var urn = await SerializeAsync($"{ctx}.urn", customResource.Urn, keepResources).ConfigureAwait(false);
var urn = await SerializeAsync($"{ctx}.urn", customResource.Urn, keepResources, keepOutputValues: false).ConfigureAwait(false);
var builder = ImmutableDictionary.CreateBuilder<string, object?>();
builder.Add(Constants.SpecialSigKey, Constants.SpecialResourceSig);
builder.Add(Constants.ResourceUrnName, urn);
@ -206,7 +252,7 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output:
Log.Debug($"Serialize property[{ctx}]: Encountered ComponentResource");
}
var urn = await SerializeAsync($"{ctx}.urn", componentResource.Urn, keepResources).ConfigureAwait(false);
var urn = await SerializeAsync($"{ctx}.urn", componentResource.Urn, keepResources, keepOutputValues: false).ConfigureAwait(false);
if (keepResources)
{
var builder = ImmutableDictionary.CreateBuilder<string, object?>();
@ -218,10 +264,14 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output:
}
if (prop is IDictionary dictionary)
return await SerializeDictionaryAsync(ctx, dictionary, keepResources).ConfigureAwait(false);
{
return await SerializeDictionaryAsync(ctx, dictionary, keepResources, keepOutputValues).ConfigureAwait(false);
}
if (prop is IList list)
return await SerializeListAsync(ctx, list, keepResources).ConfigureAwait(false);
{
return await SerializeListAsync(ctx, list, keepResources, keepOutputValues).ConfigureAwait(false);
}
if (prop is Enum e && e.GetTypeCode() == TypeCode.Int32)
{
@ -257,27 +307,27 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output:
case JsonValueKind.False:
return element.GetBoolean();
case JsonValueKind.Array:
{
var result = ImmutableArray.CreateBuilder<object?>();
var index = 0;
foreach (var child in element.EnumerateArray())
{
result.Add(SerializeJson($"{ctx}[{index}]", child));
index++;
}
var result = ImmutableArray.CreateBuilder<object?>();
var index = 0;
foreach (var child in element.EnumerateArray())
{
result.Add(SerializeJson($"{ctx}[{index}]", child));
index++;
}
return result.ToImmutable();
}
return result.ToImmutable();
}
case JsonValueKind.Object:
{
var result = ImmutableDictionary.CreateBuilder<string, object?>();
foreach (var x in element.EnumerateObject())
{
result[x.Name] = SerializeJson($"{ctx}.{x.Name}", x.Value);
}
var result = ImmutableDictionary.CreateBuilder<string, object?>();
foreach (var x in element.EnumerateObject())
{
result[x.Name] = SerializeJson($"{ctx}.{x.Name}", x.Value);
}
return result.ToImmutable();
}
return result.ToImmutable();
}
default:
throw new InvalidOperationException($"Unknown {nameof(JsonElement)}.{nameof(JsonElement.ValueKind)}: {element.ValueKind}");
}
@ -296,7 +346,7 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output:
throw new InvalidOperationException("Cannot serialize invalid archive");
var propName = assetOrArchive.PropName;
var value = await SerializeAsync(ctx + "." + propName, assetOrArchive.Value, keepResources).ConfigureAwait(false);
var value = await SerializeAsync(ctx + "." + propName, assetOrArchive.Value, keepResources, keepOutputValues: false).ConfigureAwait(false);
var builder = ImmutableDictionary.CreateBuilder<string, object>();
builder.Add(Constants.SpecialSigKey, assetOrArchive.SigKey);
@ -304,7 +354,7 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output:
return builder.ToImmutable();
}
private async Task<ImmutableDictionary<string, object>> SerializeInputArgsAsync(string ctx, InputArgs args, bool keepResources)
private async Task<ImmutableDictionary<string, object>> SerializeInputArgsAsync(string ctx, InputArgs args, bool keepResources, bool keepOutputValues)
{
if (_excessiveDebugOutput)
{
@ -312,10 +362,10 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output:
}
var dictionary = await args.ToDictionaryAsync().ConfigureAwait(false);
return await SerializeDictionaryAsync(ctx, dictionary, keepResources).ConfigureAwait(false);
return await SerializeDictionaryAsync(ctx, dictionary, keepResources, keepOutputValues).ConfigureAwait(false);
}
private async Task<ImmutableArray<object?>> SerializeListAsync(string ctx, IList list, bool keepResources)
private async Task<ImmutableArray<object?>> SerializeListAsync(string ctx, IList list, bool keepResources, bool keepOutputValues)
{
if (_excessiveDebugOutput)
{
@ -330,13 +380,13 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output:
Log.Debug($"Serialize property[{ctx}]: array[{i}] element");
}
result.Add(await SerializeAsync($"{ctx}[{i}]", list[i], keepResources).ConfigureAwait(false));
result.Add(await SerializeAsync($"{ctx}[{i}]", list[i], keepResources, keepOutputValues).ConfigureAwait(false));
}
return result.MoveToImmutable();
}
private async Task<ImmutableDictionary<string, object>> SerializeDictionaryAsync(string ctx, IDictionary dictionary, bool keepResources)
private async Task<ImmutableDictionary<string, object>> SerializeDictionaryAsync(string ctx, IDictionary dictionary, bool keepResources, bool keepOutputValues)
{
if (_excessiveDebugOutput)
{
@ -359,7 +409,7 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output:
// When serializing an object, we omit any keys with null values. This matches
// JSON semantics.
var v = await SerializeAsync($"{ctx}.{stringKey}", dictionary[stringKey], keepResources).ConfigureAwait(false);
var v = await SerializeAsync($"{ctx}.{stringKey}", dictionary[stringKey], keepResources, keepOutputValues).ConfigureAwait(false);
if (v != null)
{
result[stringKey] = v;