Skip to content

Commit da91394

Browse files
saibulusuRakeshwar Reddy KambaiahgariRakeshwarKsaibulusu
authored
Conditional parameter sets (#569)
* Commit Changes * Updated UnitTests.csproj * Updated Tests * 2 of 3 requested changes * fixing compilation error. * Moving process parmeters async to execution profile extensions. * Fixes from Bryan for ProfileExpressionEvaluator, fixes for ProfileExpressionEvaluatorTests, ExecutionProfileExtensions. --------- Co-authored-by: Rakeshwar Reddy Kambaiahgari <rkambaiahgar@microsoft.com> Co-authored-by: Rakesh <153008248+RakeshwarK@users.noreply.github.com> Co-authored-by: saibulusu <saibulusu@microsoft.com>
1 parent b1e215a commit da91394

10 files changed

Lines changed: 654 additions & 11 deletions

File tree

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace VirtualClient.Common.Contracts
5+
{
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
using Newtonsoft.Json;
10+
using Newtonsoft.Json.Linq;
11+
using VirtualClient.Common.Extensions;
12+
13+
/// <summary>
14+
/// Provides a JSON converter that can handle the serialization/deserialization of
15+
/// <see cref="List{T}"/> objects where T is <see cref="IDictionary{TKey, TValue}"/> with string keys and <see cref="IConvertible"/> values.
16+
/// </summary>
17+
public class ParameterDictionaryCollectionJsonConverter : JsonConverter
18+
{
19+
private static readonly Type ParameterDictionaryListType = typeof(List<IDictionary<string, IConvertible>>);
20+
private static readonly ParameterDictionaryJsonConverter DictionaryConverter = new ParameterDictionaryJsonConverter();
21+
22+
/// <summary>
23+
/// Returns true/false whether the object type is supported for JSON serialization/deserialization.
24+
/// </summary>
25+
/// <param name="objectType">The type of object to serialize/deserialize.</param>
26+
/// <returns>
27+
/// True if the object is supported, false if not.
28+
/// </returns>
29+
public override bool CanConvert(Type objectType)
30+
{
31+
return objectType == ParameterDictionaryListType;
32+
}
33+
34+
/// <summary>
35+
/// Reads the JSON text from the reader and converts it into a <see cref="List{T}"/> of <see cref="IDictionary{TKey, TValue}"/>
36+
/// object instance.
37+
/// </summary>
38+
/// <param name="reader">Contains the JSON text defining the list of dictionaries object.</param>
39+
/// <param name="objectType">The type of object (in practice this will only be a list of dictionaries type).</param>
40+
/// <param name="existingValue">Unused.</param>
41+
/// <param name="serializer">Unused.</param>
42+
/// <returns>
43+
/// A deserialized list of dictionaries object converted from JSON text.
44+
/// </returns>
45+
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
46+
{
47+
if (reader == null)
48+
{
49+
throw new ArgumentException("The reader parameter is required.", nameof(reader));
50+
}
51+
52+
List<IDictionary<string, IConvertible>> list = new List<IDictionary<string, IConvertible>>();
53+
if (reader.TokenType == JsonToken.StartArray)
54+
{
55+
JArray array = JArray.Load(reader);
56+
foreach (JToken item in array)
57+
{
58+
if (item.Type == JTokenType.Object)
59+
{
60+
IDictionary<string, IConvertible> dictionary = new Dictionary<string, IConvertible>();
61+
ReadDictionaryEntries(item, dictionary);
62+
list.Add(dictionary);
63+
}
64+
}
65+
}
66+
67+
return list;
68+
}
69+
70+
/// <summary>
71+
/// Writes a list of dictionaries object to JSON text.
72+
/// </summary>
73+
/// <param name="writer">Handles the writing of the JSON text.</param>
74+
/// <param name="value">The list of dictionaries object to serialize to JSON text.</param>
75+
/// <param name="serializer">The JSON serializer handling the serialization to JSON text.</param>
76+
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
77+
{
78+
writer.ThrowIfNull(nameof(writer));
79+
serializer.ThrowIfNull(nameof(serializer));
80+
81+
List<IDictionary<string, IConvertible>> list = value as List<IDictionary<string, IConvertible>>;
82+
if (list != null)
83+
{
84+
writer.WriteStartArray();
85+
foreach (var dictionary in list)
86+
{
87+
WriteDictionaryEntries(writer, dictionary, serializer);
88+
}
89+
90+
writer.WriteEndArray();
91+
}
92+
}
93+
94+
private static void ReadDictionaryEntries(JToken jsonObject, IDictionary<string, IConvertible> dictionary)
95+
{
96+
IEnumerable<JToken> children = jsonObject.Children();
97+
if (children.Any())
98+
{
99+
foreach (JToken child in children)
100+
{
101+
if (child.Type == JTokenType.Property)
102+
{
103+
if (child.First != null)
104+
{
105+
JValue propertyValue = child.First as JValue;
106+
IConvertible settingValue = propertyValue?.Value as IConvertible;
107+
108+
// JSON properties that have periods (.) in them will have a path representation
109+
// like this: ['this.is.a.path']. We have to account for that when adding the key
110+
// to the dictionary. The key we want to add is 'this.is.a.path'
111+
string key = child.Path;
112+
int lastDotIndex = key.LastIndexOf('.');
113+
if (lastDotIndex >= 0)
114+
{
115+
key = key.Substring(lastDotIndex + 1);
116+
}
117+
118+
dictionary.Add(key, settingValue);
119+
}
120+
}
121+
}
122+
}
123+
}
124+
125+
private static void WriteDictionaryEntries(JsonWriter writer, IDictionary<string, IConvertible> dictionary, JsonSerializer serializer)
126+
{
127+
writer.WriteStartObject();
128+
if (dictionary.Count > 0)
129+
{
130+
foreach (KeyValuePair<string, IConvertible> entry in dictionary)
131+
{
132+
writer.WritePropertyName(entry.Key);
133+
serializer.Serialize(writer, entry.Value);
134+
}
135+
}
136+
137+
writer.WriteEndObject();
138+
}
139+
}
140+
}

src/VirtualClient/VirtualClient.Contracts/ExecutionProfile.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ public ExecutionProfile(
3535
IEnumerable<ExecutionProfileElement> dependencies,
3636
IEnumerable<ExecutionProfileElement> monitors,
3737
IDictionary<string, IConvertible> metadata,
38-
IDictionary<string, IConvertible> parameters)
38+
IDictionary<string, IConvertible> parameters,
39+
List<IDictionary<string, IConvertible>> parametersOn = null)
3940
{
4041
description.ThrowIfNullOrWhiteSpace(nameof(description));
4142

@@ -62,6 +63,10 @@ public ExecutionProfile(
6263
? new Dictionary<string, IConvertible>(parameters, StringComparer.OrdinalIgnoreCase)
6364
: new Dictionary<string, IConvertible>(StringComparer.OrdinalIgnoreCase);
6465

66+
this.ParametersOn = parametersOn != null
67+
? parametersOn
68+
: new List<IDictionary<string, IConvertible>>();
69+
6570
if (this.Actions?.Any() == true)
6671
{
6772
this.Actions.ForEach(action => action.ComponentType = ComponentType.Action);
@@ -90,7 +95,8 @@ public ExecutionProfile(ExecutionProfile other)
9095
other?.Dependencies,
9196
other?.Monitors,
9297
other?.Metadata,
93-
other?.Parameters)
98+
other?.Parameters,
99+
other?.ParametersOn)
94100
{
95101
}
96102

@@ -106,7 +112,8 @@ public ExecutionProfile(ExecutionProfileYamlShim other)
106112
other?.Dependencies?.Select(d => new ExecutionProfileElement(d)),
107113
other?.Monitors?.Select(m => new ExecutionProfileElement(m)),
108114
other?.Metadata,
109-
other?.Parameters)
115+
other?.Parameters,
116+
other?.ParametersOn)
110117
{
111118
}
112119

@@ -148,6 +155,13 @@ public ExecutionProfile(ExecutionProfileYamlShim other)
148155
[JsonConverter(typeof(ParameterDictionaryJsonConverter))]
149156
public IDictionary<string, IConvertible> Parameters { get; }
150157

158+
/// <summary>
159+
/// List of parameter dictionaries that are associated with the profile.
160+
/// </summary>
161+
[JsonProperty(PropertyName = "ParametersOn", Required = Required.Default, Order = 75)]
162+
[JsonConverter(typeof(ParameterDictionaryCollectionJsonConverter))]
163+
public List<IDictionary<string, IConvertible>> ParametersOn { get; }
164+
151165
/// <summary>
152166
/// Workload actions to run as part of the profile execution.
153167
/// </summary>

src/VirtualClient/VirtualClient.Contracts/ExecutionProfileExtensions.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ namespace VirtualClient.Contracts
66
using System;
77
using System.Collections.Generic;
88
using System.Linq;
9+
using System.Threading.Tasks;
10+
using Microsoft.Extensions.DependencyInjection;
11+
using Microsoft.Extensions.Logging;
912
using VirtualClient.Common.Extensions;
13+
using VirtualClient.Common.Telemetry;
1014

1115
/// <summary>
1216
/// Extension methods for <see cref="ExecutionProfile"/>
@@ -28,6 +32,54 @@ public static void Inline(this ExecutionProfile profile)
2832
ExecutionProfileExtensions.InlineElements(elements, profile.Parameters);
2933
}
3034

35+
/// <summary>
36+
/// Processes conditional parameter sets in the profile's ParametersOn sections.
37+
/// </summary>
38+
/// <param name="profile">The execution profile to process.</param>
39+
/// <param name="dependencies">The service dependencies.</param>
40+
/// <returns>A task that represents the asynchronous operation.</returns>
41+
public static async Task EvaluateConditionalParametersAsync(this ExecutionProfile profile, IServiceCollection dependencies)
42+
{
43+
if (profile.ParametersOn?.Any() == true)
44+
{
45+
const string conditionKey = "Condition";
46+
var evaluator = dependencies.GetService<IExpressionEvaluator>();
47+
48+
IDictionary<string, IConvertible> profileParameters = new Dictionary<string, IConvertible>(profile.Parameters, StringComparer.OrdinalIgnoreCase);
49+
await evaluator.EvaluateAsync(dependencies, profileParameters);
50+
51+
foreach (var parametersSection in profile.ParametersOn)
52+
{
53+
if (!parametersSection.TryGetValue(conditionKey, out IConvertible condition))
54+
{
55+
throw new SchemaException(
56+
$"Invalid '{nameof(profile.ParametersOn)}' configuration. A '{conditionKey}' must be defined in each '{nameof(profile.ParametersOn)}' section.");
57+
}
58+
59+
// Parameters in ParametersOn sections take priority over the profile's default parameters.
60+
IDictionary<string, IConvertible> conditionalParameters = new Dictionary<string, IConvertible>(parametersSection, StringComparer.OrdinalIgnoreCase);
61+
conditionalParameters.AddRange(profileParameters);
62+
63+
await evaluator.EvaluateAsync(dependencies, conditionalParameters);
64+
65+
if (!bool.TryParse(conditionalParameters[conditionKey].ToString(), out bool conditionMatches))
66+
{
67+
throw new SchemaException(
68+
$"Invalid '{nameof(profile.ParametersOn)}' configuration. A '{conditionKey}' must always evaluate to true or false.");
69+
}
70+
71+
if (conditionMatches)
72+
{
73+
profile.Parameters.Clear();
74+
profile.Parameters.AddRange(conditionalParameters);
75+
break;
76+
}
77+
}
78+
79+
profile.ParametersOn.Clear();
80+
}
81+
}
82+
3183
/// <summary>
3284
/// Returns true/false whether the current executor scenario matches any defined in the
3385
/// excluded scenarios supplied.

src/VirtualClient/VirtualClient.Contracts/ExecutionProfileYamlShim.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public ExecutionProfileYamlShim()
3030
this.Monitors = new List<ExecutionProfileElementYamlShim>();
3131
this.Metadata = new Dictionary<string, IConvertible>(StringComparer.OrdinalIgnoreCase);
3232
this.Parameters = new Dictionary<string, IConvertible>(StringComparer.OrdinalIgnoreCase);
33+
this.ParametersOn = new List<IDictionary<string, IConvertible>>();
3334
}
3435

3536
/// <summary>
@@ -68,6 +69,11 @@ public ExecutionProfileYamlShim(ExecutionProfile other)
6869
this.Parameters.AddRange(other.Parameters);
6970
}
7071

72+
if (other?.ParametersOn?.Any() == true)
73+
{
74+
this.ParametersOn.AddRange(other.ParametersOn);
75+
}
76+
7177
if (this.Actions?.Any() == true)
7278
{
7379
this.Actions.ForEach(action => action.ComponentType = ComponentType.Action);
@@ -108,6 +114,12 @@ public ExecutionProfileYamlShim(ExecutionProfile other)
108114
[YamlMember(Alias = "parameters", Order = 20, ScalarStyle = ScalarStyle.Plain)]
109115
public IDictionary<string, IConvertible> Parameters { get; set; }
110116

117+
/// <summary>
118+
/// Collection of parameters that are associated with the profile.
119+
/// </summary>
120+
[YamlMember(Alias = "parameters_on", Order = 25, ScalarStyle = ScalarStyle.Plain)]
121+
public List<IDictionary<string, IConvertible>> ParametersOn { get; set; }
122+
111123
/// <summary>
112124
/// Workload actions to run as part of the profile execution.
113125
/// </summary>

0 commit comments

Comments
 (0)