From a191fe9e7ded924f1851347d0150d98173b78045 Mon Sep 17 00:00:00 2001 From: lordmilko Date: Fri, 18 Sep 2020 15:05:05 +1000 Subject: [PATCH] -Implemented -Hashtable parameter on New-SensorFactoryDefinition -Implemented type conversion on values specified to cmdlet parameters invoked by PSCmdletInvoker --- .../ObjectData/NewSensorFactoryDefinition.cs | 225 +++++++++++++++- .../Cmdlets/Session/ConnectPrtgServer.cs | 2 +- .../PowerShell/DummyRuntime.cs | 6 +- .../PowerShell/ParameterSet.cs | 2 + .../Progress/PSReflectionCacheManager.cs | 6 +- .../PowerShell/Progress/ProgressManager.cs | 2 +- .../PrtgAPI.PowerShell.csproj | 2 + .../Utilities/AstFactory.cs | 48 ++++ .../Utilities/EmptyScriptExtent.cs | 48 ++++ .../Utilities/PSCmdletInvoker.cs | 27 +- .../Utilities/PSReflectionUtilities.cs | 34 ++- .../CSharp/Infrastructure/AssemblyTests.cs | 3 +- .../New-SensorFactoryDefinition.Tests.ps1 | 249 +++++++++++++++++- .../ObjectManipulation/New-Sensor.Tests.ps1 | 102 ++++++- src/PrtgAPI/Linq/EnumerableEx.cs | 11 + src/PrtgAPI/Utilities/ReflectionExtensions.cs | 10 + 16 files changed, 734 insertions(+), 43 deletions(-) create mode 100644 src/PrtgAPI.PowerShell/Utilities/AstFactory.cs create mode 100644 src/PrtgAPI.PowerShell/Utilities/EmptyScriptExtent.cs diff --git a/src/PrtgAPI.PowerShell/PowerShell/Cmdlets/ObjectData/NewSensorFactoryDefinition.cs b/src/PrtgAPI.PowerShell/PowerShell/Cmdlets/ObjectData/NewSensorFactoryDefinition.cs index 2054c07f..6e0c2735 100644 --- a/src/PrtgAPI.PowerShell/PowerShell/Cmdlets/ObjectData/NewSensorFactoryDefinition.cs +++ b/src/PrtgAPI.PowerShell/PowerShell/Cmdlets/ObjectData/NewSensorFactoryDefinition.cs @@ -1,9 +1,16 @@ using System; +using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Management.Automation; -using PrtgAPI.PowerShell.Base; +using System.Management.Automation.Language; +using System.Reflection; +using System.Text; +using PrtgAPI.Linq; +using PrtgAPI.Reflection; +using PrtgAPI.Reflection.Cache; +using PrtgAPI.Utilities; namespace PrtgAPI.PowerShell.Cmdlets { @@ -18,8 +25,8 @@ namespace PrtgAPI.PowerShell.Cmdlets /// /// When specifying an Expression, the default expression and current sensor can be accessed via the $expr and $_ automatic variables. /// Unless you wish to modify the sensor ID or channel ID to be used for a specific sensor, it is generally recommended to avoid recalculating - /// the base channel definition and use the automatic $expr variable, which is defined as "channel(sensorId, channelID)" where sensorID is the ID of the current - /// sensor, and channelID is the value passed to -. If a - is not specified, channel ID 0 will automatically be used. + /// the base channel definition and use the automatic $expr variable, which is defined as "channel(sensorId, channelId)" where sensorId is the ID of the current + /// sensor, and channelId is the value passed to -. If a - is not specified, channel ID 0 will automatically be used. /// /// Aggregation channels, representing values derived from multiple sensor channels, can be created using the - and - parameters. /// When - is specified, New-SensorFactoryDefinition creates a single channel based on all of the channels piped in to the - parameter. By contrast, @@ -46,9 +53,15 @@ namespace PrtgAPI.PowerShell.Cmdlets /// /// Horizontal lines can be generated by specifying the position the line should appear at to the - parameter. When /// specifying a horizontal line the channel unit the line should apply to should be specified in square brackets at the end of the channel name. It - /// does not matter what the ID of any horizontal lines are, as long as they do not conflict with any other channel definitions. + /// does not matter what the ID of any horizontal lines are, as long as they do not conflict with any other channel definitions. /// - /// To automatically copy the output of New-SensorFactoryDefinition to the clipboard, you can pipe the cmdlet to clip.exe. + /// When creating a complex sensor factory definition that may require multiple invocations of New-SensorFactoryDefinition to achieve, + /// you can alternatively specify a collection of values to the - parameter, with each table containing the cmdlet parameters you would specify + /// if you were to invoke New-SensorFactoryDefinition normally. This allows you to fully describe the nature of your entire factory definition in one go, without having to worry + /// about offsetting the - for each successive invocation of New-SensorFactoryDefinition. If a - is specified + /// in a , the running channel record ID counter will effectively be overridden, with all successive channel definitions continuing from this newly specified ID. + /// + /// To automatically copy the output of New-SensorFactoryDefinition to the clipboard on Windows, you can pipe the cmdlet to clip.exe. /// /// /// C:\> Get-Sensor -Tags wmicpuloadsensor | fdef { $_.Device } @@ -76,7 +89,7 @@ namespace PrtgAPI.PowerShell.Cmdlets /// /// C:\> $sensors | fdef { $_.Device } -sn "Average CPU Load" -se { "$acc + $expr" } -sf { "$acc / $($sensors.Count)" } /// - /// Create a channel definition showing the average CPU Load of all devices as well as channel definitions for each individual device using a summary expression and finalizer formula + /// Create a channel definition showing the average CPU Load of all devices as well as channel definitions for each individual device using a custom summary expression and finalizer formula /// /// /// @@ -96,7 +109,15 @@ namespace PrtgAPI.PowerShell.Cmdlets /// /// /// C:\> fdef "Line at 40.2 [msec]" -Value 40.2 -StartId 3 - /// Create a channel definition for a horizontal line against channels that use the "msec" unit using a channel ID of 3. + /// Create a channel definition for a horizontal line against channels that use the "msec" unit using a starting channel record ID of 3. + /// + /// + /// + /// + /// C:\> $sensors | Get-Sensor -Tags wmiterm* + /// + /// C:\> $sensors | fdef @{name={$_.Device + " Total Sessions"}; sn="Total Sessions"; se="Sum"},@{name={$_.Device + " Active Sessions"}; sn="Active Sessions"; se="Sum"; channelId=1} + /// Create a channel definition for WMI Terminal Services sensors, showing the total number of sessions overall/on each server and the active number of sessions overall/on each server. /// /// /// Online version: @@ -113,6 +134,7 @@ public class NewSensorFactoryDefinition : PSCmdlet [Parameter(Mandatory = true, ValueFromPipeline = true, ParameterSetName = ParameterSet.Default)] [Parameter(Mandatory = true, ValueFromPipeline = true, ParameterSetName = ParameterSet.Aggregate)] [Parameter(Mandatory = true, ValueFromPipeline = true, ParameterSetName = ParameterSet.Summary)] + [Parameter(Mandatory = false, ValueFromPipeline = true, ParameterSetName = ParameterSet.HashTable)] public Sensor Sensor { get; set; } /// @@ -153,6 +175,7 @@ public class NewSensorFactoryDefinition : PSCmdlet [Parameter(Mandatory = false, ParameterSetName = ParameterSet.Aggregate)] [Parameter(Mandatory = false, ParameterSetName = ParameterSet.Summary)] [Parameter(Mandatory = false, ParameterSetName = ParameterSet.Manual)] + [Parameter(Mandatory = false, ParameterSetName = ParameterSet.HashTable)] public int StartId { get; set; } = 1; /// @@ -205,6 +228,13 @@ public class NewSensorFactoryDefinition : PSCmdlet [Parameter(Mandatory = false, ParameterSetName = ParameterSet.Summary)] public ScriptBlock SummaryFinalizer { get; set; } + /// + /// A collection of one or more hashtable objects specifying the parameters to use for performing one or more invocations + /// of New-SensorFactoryDefinition, while retaining the latest channel ID across each invocation. + /// + [Parameter(Mandatory = true, Position = 0, ParameterSetName = ParameterSet.HashTable)] + public Hashtable[] HashTable { get; set; } + private int id; private readonly PSVariable accumulation = new PSVariable("acc"); private List summarySensors = new List(); @@ -242,6 +272,10 @@ protected override void ProcessRecord() case ParameterSet.Summary: summarySensors.Add(Sensor); break; + case ParameterSet.HashTable: + if (Sensor != null) + summarySensors.Add(Sensor); + break; case ParameterSet.Default: case ParameterSet.Manual: ProcessNormal(Sensor); @@ -290,6 +324,9 @@ protected override void EndProcessing() case ParameterSet.Summary: ProcessSummary(); break; + case ParameterSet.HashTable: + ProcessHashTable(); + break; default: throw new UnknownParameterSetException(ParameterSetName); } @@ -346,6 +383,180 @@ private void ProcessSummary() ProcessNormal(sensor); } + private void ProcessHashTable() + { + id = StartId; + + foreach(var table in HashTable) + { + var dict = table.ToDictionary(); + + var bindingResult = GetStaticBindingResult(dict); + + var boundParameters = GetInvokerBoundParameters(bindingResult); + var parameterSetName = GetInvokerParameterSetName(bindingResult); + + if (!boundParameters.ContainsKey(nameof(StartId))) + boundParameters[nameof(StartId)] = id; + + ValidateMandatoryParameters(boundParameters, parameterSetName, dict); + + var instance = new NewSensorFactoryDefinition(); + + var invoker = new PSCmdletInvoker(this, instance, parameterSetName, (c, b) => + { + object s; + + //Not all parameter sets (e.g. Manual) require a sensor! + if (b.TryGetValue(nameof(Sensor), out s)) + c.Sensor = (Sensor)s; + }); + + invoker.BindParameters(boundParameters); + invoker.BeginProcessing(boundParameters); + + if (boundParameters.ContainsKey(nameof(Sensor))) + { + foreach (var sensor in summarySensors) + { + boundParameters[nameof(Sensor)] = sensor; + invoker.ProcessRecord(boundParameters); + } + } + else + { + invoker.ProcessRecord(boundParameters); + } + + invoker.EndProcessing(); + + id = instance.id; + + foreach(var item in invoker.Output) + WriteObject(item); + } + } + + private void ValidateMandatoryParameters(Dictionary boundParameters, string parameterSetName, Dictionary dict) + { + var properties = ReflectionCacheManager.Get(GetType()).Properties.Where(p => p.GetAttribute() != null).ToList(); + + var required = properties.Select( + p => Tuple.Create( + p.Property.Name, + p.GetAttributes().Where(a => a.ParameterSetName == parameterSetName && a.Mandatory).FirstOrDefault() + ) + ).Where(t => t.Item2 != null).Select(t => t.Item1).ToArray(); + + var missing = required.Except(boundParameters.Keys).ToArray(); + + if (missing.Length > 0) + throw new ParameterBindingException($"Cannot process hashtable '{PrettyPrintHashtable(dict)}' on parameter set '{parameterSetName}': parameter '{missing[0]}' is mandatory."); + } + + private string PrettyPrintHashtable(Dictionary dict) + { + var builder = new StringBuilder(); + + builder.Append("@{"); + + builder.Append(string.Join("; ", dict.Select(e => $"{e.Key}={FormatValue(e.Value)}"))); + + builder.Append("}"); + + return builder.ToString(); + } + + private string FormatValue(object v) + { + if (v is ScriptBlock) + return $"{{{v}}}"; + + if (v is string) + return $"\"{v}\""; + + return v?.ToString(); + } + + private Dictionary GetInvokerBoundParameters(StaticBindingResult bindingResult) + { + var dict = bindingResult.BoundParameters.Select(p => Tuple.Create(p.Key, p.Value.ConstantValue)).ToDictionary(i => i.Item1, i => i.Item2); + + var bindingInfo = GetBindingInfo(bindingResult); + + var boundArguments = (IDictionary) bindingInfo.GetInternalProperty("BoundArguments"); + + if (summarySensors.Count > 0 && boundArguments.Contains(nameof(Sensor))) + dict[nameof(Sensor)] = null; + + return dict; + } + + private string GetInvokerParameterSetName(StaticBindingResult bindingResult) + { + var bindingInfo = GetBindingInfo(bindingResult); + + var boundParameters = bindingInfo.GetInternalProperty("BoundParameters").ToIEnumerable().ToArray(); + var parameterSetFlag = (uint)bindingInfo.GetInternalProperty("ValidParameterSetsFlags"); + + var parameterSetData = boundParameters[0] + .GetInternalField("value") + .GetInternalProperty("Parameter") + .GetInternalProperty("ParameterSetData") + .ToIEnumerable() + .ToArray(); + + var parameterSetFlags = parameterSetData + .Select(p => Tuple.Create( + (string)p.GetPublicProperty("Key"), + (uint)p.GetPublicProperty("Value").GetInternalProperty("ParameterSetFlag") + )).ToArray(); + + var selectedSet = parameterSetFlags.First(f => f.Item2 == parameterSetFlag).Item1; + + return selectedSet; + } + + private object GetBindingInfo(StaticBindingResult bindingResult) + { + return bindingResult.PSGetInternalField("_bindingInfo", "bindingInfo", this); + } + + private StaticBindingResult GetStaticBindingResult(Dictionary dict) + { + var attrib = typeof(NewSensorFactoryDefinition).GetCustomAttribute(); + var cmdletName = $"{attrib.VerbName}-{attrib.NounName}"; + + var commandElements = new List + { + AstFactory.StringConstantExpression(cmdletName, StringConstantType.BareWord) + }; + + foreach (var item in dict) + { + commandElements.Add(AstFactory.CommandParameter(item.Key.ToString(), null, new EmptyScriptExtent(true))); + commandElements.Add(AstFactory.ConstantExpression(item.Value)); + } + + var commandPipeline = AstFactory.Pipeline( + AstFactory.CommandExpression( + AstFactory.VariableExpression("sensors") + ), + AstFactory.Command(commandElements) + ); + + var bindingResult = StaticParameterBinder.BindCommand((CommandAst) commandPipeline.PipelineElements[1]); + + if (bindingResult.BindingExceptions.Count > 0) + { + var exception = bindingResult.BindingExceptions.First().Value.BindingException; + + throw new ParameterBindingException($"Failed to parse hashtable '{PrettyPrintHashtable(dict)}': {exception.Message.EnsurePeriod()}", exception); + } + + return bindingResult; + } + private Tuple GetScriptBlockFromSummaryMode(EnumOrScriptBlock summary, ScriptBlock finalizer) { ScriptBlock agg = null; diff --git a/src/PrtgAPI.PowerShell/PowerShell/Cmdlets/Session/ConnectPrtgServer.cs b/src/PrtgAPI.PowerShell/PowerShell/Cmdlets/Session/ConnectPrtgServer.cs index 8a856b56..53ce4292 100644 --- a/src/PrtgAPI.PowerShell/PowerShell/Cmdlets/Session/ConnectPrtgServer.cs +++ b/src/PrtgAPI.PowerShell/PowerShell/Cmdlets/Session/ConnectPrtgServer.cs @@ -223,7 +223,7 @@ internal void Connect(PSCmdlet cmdlet) } } - private PSEdition GetPSEdition(PSCmdlet cmdlet) + internal static PSEdition GetPSEdition(PSCmdlet cmdlet) { var variable = cmdlet.GetVariableValue("global:PSEdition")?.ToString(); diff --git a/src/PrtgAPI.PowerShell/PowerShell/DummyRuntime.cs b/src/PrtgAPI.PowerShell/PowerShell/DummyRuntime.cs index f81d6280..e0809593 100644 --- a/src/PrtgAPI.PowerShell/PowerShell/DummyRuntime.cs +++ b/src/PrtgAPI.PowerShell/PowerShell/DummyRuntime.cs @@ -11,7 +11,7 @@ namespace PrtgAPI.PowerShell [ExcludeFromCodeCoverage] class DummyRuntime : ICommandRuntime { - internal static long _lastUsedSourceId => (long) typeof(PSCmdlet).Assembly.GetType("System.Management.Automation.MshCommandRuntime").PSGetInternalStaticField("s_lastUsedSourceId", "_lastUsedSourceId"); + internal static long _lastUsedSourceId => (long) typeof(PSCmdlet).Assembly.GetType("System.Management.Automation.MshCommandRuntime").PSGetInternalStaticField("s_lastUsedSourceId", "_lastUsedSourceId", null); public List Output { get; } = new List(); @@ -21,9 +21,9 @@ class DummyRuntime : ICommandRuntime internal object OutputPipe => Owner.CommandRuntime.GetInternalProperty("OutputPipe"); - internal PrtgCmdlet Owner { get; } + internal PSCmdlet Owner { get; } - public DummyRuntime(PrtgCmdlet Owner) + public DummyRuntime(PSCmdlet Owner) { this.Owner = Owner; } diff --git a/src/PrtgAPI.PowerShell/PowerShell/ParameterSet.cs b/src/PrtgAPI.PowerShell/PowerShell/ParameterSet.cs index b0f4f7d8..55af5db8 100644 --- a/src/PrtgAPI.PowerShell/PowerShell/ParameterSet.cs +++ b/src/PrtgAPI.PowerShell/PowerShell/ParameterSet.cs @@ -126,5 +126,7 @@ static class ParameterSet internal const string ManualWithManual = "ManualWithManualSet"; internal const string Object = "ObjectSet"; + + internal const string HashTable = "HashTableSet"; } } diff --git a/src/PrtgAPI.PowerShell/PowerShell/Progress/PSReflectionCacheManager.cs b/src/PrtgAPI.PowerShell/PowerShell/Progress/PSReflectionCacheManager.cs index 49ece820..0cc50b3e 100644 --- a/src/PrtgAPI.PowerShell/PowerShell/Progress/PSReflectionCacheManager.cs +++ b/src/PrtgAPI.PowerShell/PowerShell/Progress/PSReflectionCacheManager.cs @@ -165,7 +165,7 @@ private PrtgCmdlet GetPreviousPrtgCmdletInternal() } if (cmdlet.CommandRuntime is DummyRuntime) - return ((DummyRuntime) cmdlet.CommandRuntime).Owner; + return (PrtgCmdlet) ((DummyRuntime) cmdlet.CommandRuntime).Owner; return null; } @@ -414,7 +414,7 @@ public Pipeline GetSelectPipelineOutput() { var command = (PSCmdlet)GetUpstreamCmdletNotOfType(); - var queue = (Queue) command.PSGetInternalField("_selectObjectQueue", "selectObjectQueue"); + var queue = (Queue) command.PSGetInternalField("_selectObjectQueue", "selectObjectQueue", command); var cmdletPipeline = GetCmdletPipelineInput(); @@ -465,7 +465,7 @@ private static Pipeline GetCmdletPipelineInput(ICommandRuntime commandRuntime, I if (declaringType == typeof(Array) || enumeratorType.Name == "SZArrayEnumerator") //It's a SZArrayEnumerator (piping straight from a variable). In .NET Core 3.1 SZArrayEnumerator is no longer nested list = ((object[]) enumerator.GetInternalField("_array")); else if (declaringType == typeof(List<>)) //It's a List.Enumerator (piping from $groups[0].Group) - list = enumerator.PSGetInternalField("_list", "list").ToIEnumerable(); + list = enumerator.PSGetInternalField("_list", "list", null).ToIEnumerable(); else if (enumeratorType.IsGenericType && enumeratorType.GetGenericTypeDefinition() == typeof(ReadOnlyListEnumerator<>)) list = enumerator.GetInternalField("list").ToIEnumerable(); else diff --git a/src/PrtgAPI.PowerShell/PowerShell/Progress/ProgressManager.cs b/src/PrtgAPI.PowerShell/PowerShell/Progress/ProgressManager.cs index 4c764681..a8e03fb6 100644 --- a/src/PrtgAPI.PowerShell/PowerShell/Progress/ProgressManager.cs +++ b/src/PrtgAPI.PowerShell/PowerShell/Progress/ProgressManager.cs @@ -868,7 +868,7 @@ internal long GetLastSourceId() if (cmdlet.CommandRuntime is DummyRuntime) val = DummyRuntime._lastUsedSourceId; else - val = Convert.ToInt64(cmdlet.CommandRuntime.PSGetInternalStaticField("s_lastUsedSourceId", "_lastUsedSourceId")); + val = Convert.ToInt64(cmdlet.CommandRuntime.PSGetInternalStaticField("s_lastUsedSourceId", "_lastUsedSourceId", cmdlet)); if (PipeFromVariableWithProgress && FirstInChain) { diff --git a/src/PrtgAPI.PowerShell/PrtgAPI.PowerShell.csproj b/src/PrtgAPI.PowerShell/PrtgAPI.PowerShell.csproj index 52e8e135..e3a76c97 100644 --- a/src/PrtgAPI.PowerShell/PrtgAPI.PowerShell.csproj +++ b/src/PrtgAPI.PowerShell/PrtgAPI.PowerShell.csproj @@ -202,6 +202,8 @@ + + diff --git a/src/PrtgAPI.PowerShell/Utilities/AstFactory.cs b/src/PrtgAPI.PowerShell/Utilities/AstFactory.cs new file mode 100644 index 00000000..52c001c3 --- /dev/null +++ b/src/PrtgAPI.PowerShell/Utilities/AstFactory.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Management.Automation.Language; + +namespace PrtgAPI.Utilities +{ + class AstFactory + { + public static CommandAst Command(params CommandElementAst[] commandElements) + { + return Command((IEnumerable)commandElements); + } + + public static CommandAst Command(IEnumerable commandElements, TokenKind invocationOperator = TokenKind.Unknown, IEnumerable redirections = null) + { + return new CommandAst(new EmptyScriptExtent(true), commandElements, invocationOperator, redirections); + } + + public static CommandExpressionAst CommandExpression(ExpressionAst expression, IEnumerable redirections = null) + { + return new CommandExpressionAst(new EmptyScriptExtent(true), expression, redirections); + } + + public static CommandParameterAst CommandParameter(string parameterName, ExpressionAst argument, IScriptExtent errorPosition) + { + return new CommandParameterAst(new EmptyScriptExtent(true), parameterName, argument, errorPosition); + } + + public static ConstantExpressionAst ConstantExpression(object value) + { + return new ConstantExpressionAst(new EmptyScriptExtent(true), value); + } + + public static PipelineAst Pipeline(params CommandBaseAst[] pipelineElements) + { + return new PipelineAst(new EmptyScriptExtent(true), pipelineElements); + } + + public static StringConstantExpressionAst StringConstantExpression(string value, StringConstantType stringConstantType) + { + return new StringConstantExpressionAst(new EmptyScriptExtent(true), value, stringConstantType); + } + + public static VariableExpressionAst VariableExpression(string variableName) + { + return new VariableExpressionAst(new EmptyScriptExtent(true), variableName, false); + } + } +} diff --git a/src/PrtgAPI.PowerShell/Utilities/EmptyScriptExtent.cs b/src/PrtgAPI.PowerShell/Utilities/EmptyScriptExtent.cs new file mode 100644 index 00000000..50c94b52 --- /dev/null +++ b/src/PrtgAPI.PowerShell/Utilities/EmptyScriptExtent.cs @@ -0,0 +1,48 @@ +using System; +using System.Management.Automation.Language; + +namespace PrtgAPI.Utilities +{ + class EmptyScriptExtent : IScriptExtent + { + bool useDefault; + + public EmptyScriptExtent(bool useDefault) + { + this.useDefault = useDefault; + } + + public string File => GetValue(); + + public IScriptPosition StartScriptPosition => GetValue(); + + public IScriptPosition EndScriptPosition => GetValue(); + + public int StartLineNumber => GetValue(); + + public int StartColumnNumber => GetValue(); + + public int EndLineNumber => GetValue(); + + public int EndColumnNumber => GetValue(); + + public string Text => GetValue(); + + public int StartOffset => GetValue(); + + public int EndOffset => GetValue(); + + private T GetValue() + { + if (useDefault) + { + if (typeof(T) == typeof(string)) + return (T) (object) string.Empty; + + return default(T); + } + + throw new NotImplementedException(); + } + } +} diff --git a/src/PrtgAPI.PowerShell/Utilities/PSCmdletInvoker.cs b/src/PrtgAPI.PowerShell/Utilities/PSCmdletInvoker.cs index 3ef7945a..ba0afa89 100644 --- a/src/PrtgAPI.PowerShell/Utilities/PSCmdletInvoker.cs +++ b/src/PrtgAPI.PowerShell/Utilities/PSCmdletInvoker.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Management.Automation; using PrtgAPI.PowerShell; -using PrtgAPI.PowerShell.Base; +using PrtgAPI.PowerShell.Cmdlets; using PrtgAPI.Reflection; using PrtgAPI.Reflection.Cache; @@ -39,12 +39,19 @@ static PSCmdletInvoker() memberwiseClone = ReflectionExtensions.CreateFunc("MemberwiseClone"); } - public PSCmdletInvoker(PrtgCmdlet owner, PSCmdlet cmdlet, string parameterSetName, Action> valueFromPipeline = null) : + /// + /// Initializes a new instance of the class with a known parameter set name. + /// + /// The outer cmdlet that is invoking the inner cmdlet. + /// The inner cmdlet to invoke. + /// The parameter set name of the inner cmdlet to use. + /// An action that takes the inner cmdlet and its bound parameters as arguments and updates the current pipeline element that should be processed on each invocation of . + public PSCmdletInvoker(PSCmdlet owner, PSCmdlet cmdlet, string parameterSetName, Action> valueFromPipeline = null) : this(owner, cmdlet, new Lazy(() => parameterSetName), valueFromPipeline) { } - public PSCmdletInvoker(PrtgCmdlet owner, PSCmdlet cmdlet, Lazy parameterSetName, Action> valueFromPipeline) + public PSCmdletInvoker(PSCmdlet owner, PSCmdlet cmdlet, Lazy parameterSetName, Action> valueFromPipeline) { this.owner = owner; CmdletToInvoke = cmdlet; @@ -53,6 +60,10 @@ public PSCmdletInvoker(PrtgCmdlet owner, PSCmdlet cmdlet, Lazy parameter this.parameterSetName = parameterSetName; } + /// + /// Performs a complete invocation of a cmdlet. Do not use this method if you have multiple pipeline values to process. + /// + /// The bound parameters to use for the invocation. public void Invoke(Dictionary boundParameters) { BindParameters(boundParameters); @@ -63,6 +74,11 @@ public void Invoke(Dictionary boundParameters) public void BindParameters(Dictionary boundParameters) { + //If the PSEdition has not been initialized, initialize it now so that the inner cmdlet does not explode trying to initialize + //the edition due to not being a real cmdlet so not being able to access the PowerShell session state + if (PrtgSessionState.PSEdition == null) + PrtgSessionState.PSEdition = ConnectPrtgServer.GetPSEdition(owner); + var properties = ReflectionCacheManager.Get(CmdletToInvoke.GetType()).Properties.Where(p => p.GetAttribute() != null).ToArray(); foreach (var property in properties) @@ -73,6 +89,9 @@ public void BindParameters(Dictionary boundParameters) if ((description != null && boundParameters.TryGetValue(description.Description, out propertyValue)) || boundParameters.TryGetValue(property.Property.Name, out propertyValue)) { + if (propertyValue != null && property.Property.PropertyType != propertyValue.GetType()) + propertyValue = LanguagePrimitives.ConvertTo(propertyValue, property.Property.PropertyType); + property.SetValue(CmdletToInvoke, propertyValue); } } @@ -83,7 +102,7 @@ public void BeginProcessing(Dictionary boundParameters) var method = CmdletToInvoke.GetInternalMethod("SetParameterSetName"); method.Invoke(CmdletToInvoke, new[] { parameterSetName.Value }); - var myInvocationInfo = CmdletToInvoke.GetType().PSGetInternalFieldInfoFromBase("_myInvocation", "myInvocation"); + var myInvocationInfo = CmdletToInvoke.GetType().PSGetInternalFieldInfoFromBase("_myInvocation", "myInvocation", CmdletToInvoke); var newInvocation = (InvocationInfo) memberwiseClone(owner.MyInvocation); var boundParametersInfo = newInvocation.GetPublicPropertyInfo("BoundParameters"); diff --git a/src/PrtgAPI.PowerShell/Utilities/PSReflectionUtilities.cs b/src/PrtgAPI.PowerShell/Utilities/PSReflectionUtilities.cs index 943e6078..376944cc 100644 --- a/src/PrtgAPI.PowerShell/Utilities/PSReflectionUtilities.cs +++ b/src/PrtgAPI.PowerShell/Utilities/PSReflectionUtilities.cs @@ -1,41 +1,59 @@ using System; +using System.Management.Automation; using System.Reflection; using PrtgAPI.PowerShell; +using PrtgAPI.PowerShell.Cmdlets; namespace PrtgAPI.Reflection { static class PSReflectionUtilities { - public static object PSGetInternalStaticField(this object obj, string core, string desktop) + public static object PSGetInternalStaticField(this object obj, string core, string desktop, PSCmdlet cmdlet) { - if (PrtgSessionState.PSEdition == PSEdition.Core) + var edition = GetEdition(cmdlet); + + if (edition == PSEdition.Core) return obj.GetInternalStaticField(core); return obj.GetInternalStaticField(desktop); } - public static object PSGetInternalStaticField(this Type type, string core, string desktop) + public static object PSGetInternalStaticField(this Type type, string core, string desktop, PSCmdlet cmdlet) { - if (PrtgSessionState.PSEdition == PSEdition.Core) + var edition = GetEdition(cmdlet); + + if (edition == PSEdition.Core) return type.GetInternalStaticField(core); return type.GetInternalStaticField(desktop); } - public static object PSGetInternalField(this object obj, string core, string desktop) + public static object PSGetInternalField(this object obj, string core, string desktop, PSCmdlet cmdlet) { - if (PrtgSessionState.PSEdition == PSEdition.Core) + var edition = GetEdition(cmdlet); + + if (edition == PSEdition.Core) return obj.GetInternalField(core); return obj.GetInternalField(desktop); } - public static FieldInfo PSGetInternalFieldInfoFromBase(this Type type, string core, string desktop) + public static FieldInfo PSGetInternalFieldInfoFromBase(this Type type, string core, string desktop, PSCmdlet cmdlet) { - if (PrtgSessionState.PSEdition == PSEdition.Core) + var edition = GetEdition(cmdlet); + + if (edition == PSEdition.Core) return type.GetInternalFieldInfoFromBase(core); return type.GetInternalFieldInfoFromBase(desktop); } + + private static PSEdition GetEdition(PSCmdlet cmdlet) + { + if (PrtgSessionState.PSEdition != null) + return PrtgSessionState.PSEdition.Value; + + return ConnectPrtgServer.GetPSEdition(cmdlet); + } } } diff --git a/src/PrtgAPI.Tests.UnitTests/CSharp/Infrastructure/AssemblyTests.cs b/src/PrtgAPI.Tests.UnitTests/CSharp/Infrastructure/AssemblyTests.cs index bab6083b..cdc26805 100644 --- a/src/PrtgAPI.Tests.UnitTests/CSharp/Infrastructure/AssemblyTests.cs +++ b/src/PrtgAPI.Tests.UnitTests/CSharp/Infrastructure/AssemblyTests.cs @@ -755,7 +755,8 @@ private void AllExceptionMessages_EndInAPeriodInternal(string file, SyntaxTree t "ex", "(Exception) null", "Object", - "Parameters" + "Parameters", + "exception" }; if (str[0] == "paramName" && str.Count == 2) diff --git a/src/PrtgAPI.Tests.UnitTests/PowerShell/ObjectData/New-SensorFactoryDefinition.Tests.ps1 b/src/PrtgAPI.Tests.UnitTests/PowerShell/ObjectData/New-SensorFactoryDefinition.Tests.ps1 index 075a4242..1fcb69c0 100644 --- a/src/PrtgAPI.Tests.UnitTests/PowerShell/ObjectData/New-SensorFactoryDefinition.Tests.ps1 +++ b/src/PrtgAPI.Tests.UnitTests/PowerShell/ObjectData/New-SensorFactoryDefinition.Tests.ps1 @@ -5,8 +5,10 @@ Describe "New-SensorFactoryDefinition" -Tag @("PowerShell", "UnitTest") { $sensors = Run "Sensor" { $item1 = GetItem + $item1.Device = "dc1" $item1.ObjId = 1001 $item2 = GetItem + $item2.Device = "dc2" $item2.ObjId = 1002 WithItems($item1, $item2) { @@ -34,28 +36,28 @@ Describe "New-SensorFactoryDefinition" -Tag @("PowerShell", "UnitTest") { "channel(1001,0) + channel(1002,0)" "#2:dc1" "channel(1001,0)" - "#3:dc1" + "#3:dc2" "channel(1002,0)") -join "`n" } @{ Name = "Max"; Expected = @("#1:Summary" "max(channel(1001,0), channel(1002,0))" "#2:dc1" "channel(1001,0)" - "#3:dc1" + "#3:dc2" "channel(1002,0)") -join "`n" } @{ Name = "Min"; Expected = @("#1:Summary" "min(channel(1001,0), channel(1002,0))" "#2:dc1" "channel(1001,0)" - "#3:dc1" + "#3:dc2" "channel(1002,0)") -join "`n" } @{ Name = "Average"; Expected = @("#1:Summary" "(channel(1001,0) + channel(1002,0)) / 2" "#2:dc1" "channel(1001,0)" - "#3:dc1" + "#3:dc2" "channel(1002,0)") -join "`n" } ) @@ -64,7 +66,7 @@ Describe "New-SensorFactoryDefinition" -Tag @("PowerShell", "UnitTest") { $expected = "#1:dc1`n" + "channel(1001,1)`n" + - "#2:dc1`n" + + "#2:dc2`n" + "channel(1002,1)" ( @@ -77,7 +79,7 @@ Describe "New-SensorFactoryDefinition" -Tag @("PowerShell", "UnitTest") { $expected = "#1:dc1`n" + "channel(1001,0)`n" + - "#2:dc1`n" + + "#2:dc2`n" + "channel(1002,0)" ( @@ -90,7 +92,7 @@ Describe "New-SensorFactoryDefinition" -Tag @("PowerShell", "UnitTest") { $expected = "#1:dc1 [bananas]`n" + "channel(1001,0)`n" + - "#2:dc1 [bananas]`n" + + "#2:dc2 [bananas]`n" + "channel(1002,0)" ( @@ -103,7 +105,7 @@ Describe "New-SensorFactoryDefinition" -Tag @("PowerShell", "UnitTest") { $expected = "#2:dc1`n" + "channel(1001,2)`n" + - "#3:dc1`n" + + "#3:dc2`n" + "channel(1002,2)" ( @@ -148,7 +150,7 @@ Describe "New-SensorFactoryDefinition" -Tag @("PowerShell", "UnitTest") { $expected = "#1:dc1`n" + "channel(1001,0)`n" + - "#2:dc1`n" + + "#2:dc2`n" + "channel(1002,0)" ( @@ -163,7 +165,7 @@ Describe "New-SensorFactoryDefinition" -Tag @("PowerShell", "UnitTest") { $expected = "#1:dc1`n" + "100 - channel(1001,2)`n" + - "#2:dc1`n" + + "#2:dc2`n" + "100 - channel(1002,2)" ( @@ -176,7 +178,7 @@ Describe "New-SensorFactoryDefinition" -Tag @("PowerShell", "UnitTest") { $expected = "#1:dc1`n" + "channel(1001,100)`n" + - "#2:dc1`n" + + "#2:dc2`n" + "channel(1002,100)" ( @@ -380,7 +382,7 @@ Describe "New-SensorFactoryDefinition" -Tag @("PowerShell", "UnitTest") { "(channel(1001,0) + channel(1002,0))/2`n" + "#2:dc1`n" + "channel(1001,0)`n" + - "#3:dc1`n" + + "#3:dc2`n" + "channel(1002,0)" ( @@ -393,7 +395,7 @@ Describe "New-SensorFactoryDefinition" -Tag @("PowerShell", "UnitTest") { "channel(1001,0) + channel(1002,0)`n" + "#2:dc1`n" + "channel(1001,0)`n" + - "#3:dc1`n" + + "#3:dc2`n" + "channel(1002,0)" ( @@ -406,7 +408,7 @@ Describe "New-SensorFactoryDefinition" -Tag @("PowerShell", "UnitTest") { "(channel(1001,0) + channel(1002,0))/2`n" + "#2:dc1`n" + "channel(1001,0)`n" + - "#3:dc1`n" + + "#3:dc2`n" + "channel(1002,0)" ( @@ -434,4 +436,223 @@ Describe "New-SensorFactoryDefinition" -Tag @("PowerShell", "UnitTest") { { $sensors | fdef { $_.Device } -SummaryName "Summary" -SummaryExpression { "$acc + $expr" } -SummaryFinalizer { } } | Should Throw "'' is not a valid channel expression" } } + + Context "Hashtable" { + + function Validate($expected, $table) + { + ($sensors | New-SensorFactoryDefinition $table) -Join "`n" | Should Be $expected + } + + It "specifies Default" { + + $expected = "#1:dc1`n" + + "channel(1001,0)`n" + + "#2:dc2`n" + + "channel(1002,0)" + + $table = @{ + Name = { $_.Device } + } + + Validate $expected $table + } + + It "specifies Aggregate" { + $expected = "#1:Sum`n" + + "channel(1001,0) + channel(1002,0)" + + $table = @{ + Name = "Sum" + Agg = {"$acc + $expr"} + } + + Validate $expected $table + } + + It "specifies Summary" { + $expected = "#1:Sum`n" + + "channel(1001,0) + channel(1002,0)`n" + + "#2:dc1`n" + + "channel(1001,0)`n" + + "#3:dc2`n" + + "channel(1002,0)" + + $table = @{ + Name = {$_.Device} + sn = "Sum" + se = "Sum" + } + + Validate $expected $table + } + + It "specifies Manual" { + $expected = "#1:Line at 40.2`n" + + "40.2" + + $table = @{ + Name = "Line at 40.2" + Value = 40.2 + } + + Validate $expected $table + } + + It "specifies a parameter set that requires piped sensors and one that doesn't" { + + $expected = "#1:dc1`n" + + "channel(1001,0)`n" + + "#2:dc2`n" + + "channel(1002,0)`n" + + "#3:Line at 40.2`n" + + "40.2" + + $table1 = @{ + Name = { $_.Device } + } + + $table2 = @{ + Name = "Line at 40.2" + Value = 40.2 + } + + Validate $expected $table1,$table2 + } + + It "specifies nested Hashtable" { + $expected = "#1:dc1`n" + + "channel(1001,0)`n" + + "#2:dc2`n" + + "channel(1002,0)`n" + + "#3:Line at 40.2`n" + + "40.2" + + $table1 = @{ + Hashtable = @{ + Name = {$_.Device} + } + } + + $table2 = @{ + Name = "Line at 40.2" + Value = 40.2 + } + + Validate $expected $table1,$table2 + } + + It "specifies multiple Hashtables" { + $expected = "#1:Total`n" + + "channel(1001,0) + channel(1002,0)`n" + + "#2:dc1 Total`n" + + "channel(1001,0)`n" + + "#3:dc2 total`n" + + "channel(1002,0)`n" + + "#4:Inactive`n" + + "channel(1001,1) + channel(1002,1)`n" + + "#5:dc1 Inactive`n" + + "channel(1001,1)`n" + + "#6:dc2 Inactive`n" + + "channel(1002,1)" + + $table1 = @{ + name = {$_.Device + " Total"} + sn = "Total" + se = "Sum" + } + + $table2 = @{ + name = {$_.Device + " Inactive"} + sn = "Inactive" + se = "Sum" + channelid = 1 + } + + Validate $expected $table1,$table2 + } + + It "specifies a custom start ID to the cmdlet" { + $expected = "#2:dc1`n" + + "channel(1001,0)`n" + + "#3:dc2`n" + + "channel(1002,0)" + + $table = @{ + Name = { $_.Device } + } + + ($sensors | New-SensorFactoryDefinition $table -StartId 2) -Join "`n" | Should Be $expected + } + + It "specifies a custom start ID to the cmdlet and the first hashtable" { + $expected = "#3:dc1`n" + + "channel(1001,0)`n" + + "#4:dc2`n" + + "channel(1002,0)" + + $table = @{ + Name = { $_.Device } + start = 3 + } + + ($sensors | New-SensorFactoryDefinition $table -StartId 2) -Join "`n" | Should Be $expected + } + + It "specifies a custom start ID to the cmdlet and the second hashtable" { + $expected = "#2:dc1 First`n" + + "channel(1001,0)`n" + + "#3:dc2 First`n" + + "channel(1002,0)`n" + + "#6:dc1 Second`n" + + "channel(1001,1)`n" + + "#7:dc2 Second`n" + + "channel(1002,1)" + + $table1 = @{name={$_.Device + " First"}} + $table2 = @{name={$_.Device + " Second"}; start = 6; channelid = 1} + + ($sensors | New-SensorFactoryDefinition $table1,$table2 -StartId 2) -Join "`n" | Should Be $expected + } + + It "returns the same results when splatting a hashtable" { + + $expected = "#1:Sum`n" + + "channel(1001,0) + channel(1002,0)`n" + + "#2:dc1`n" + + "channel(1001,0)`n" + + "#3:dc2`n" + + "channel(1002,0)" + + $table = @{ + Name = {$_.Device} + sn = "Sum" + se = "Sum" + } + + ($sensors | New-SensorFactoryDefinition $table) -Join "`n" | Should Be $expected + ($sensors | New-SensorFactoryDefinition @table) -Join "`n" | Should Be $expected + + } + + It "throws when sensors aren't piped to a parameter set that requires them" { + + { New-SensorFactoryDefinition @{name={$_.Device}} } | Should Throw "Cannot process hashtable '@{name={`$_.Device}}' on parameter set 'DefaultSet': parameter 'Sensor' is mandatory." + } + + It "throws when an ambiguous parameter set is specified" { + + { New-SensorFactoryDefinition @{name={$_.Device}; Agg={"$acc + $expr"}; sn = "Sum"} } | Should Throw "Parameter set cannot be resolved using the specified named parameters." + } + + It "throws when an unknown parameter is specified" { + + { New-SensorFactoryDefinition @{name={$_.Device}; potato=1} } | Should Throw "A parameter cannot be found that matches parameter name 'potato'." + } + + It "throws when a mandatory parameter is unspecified" { + + { $sensors | New-SensorFactoryDefinition @{name={$_.Device}; sn="Sum"} } | Should Throw "on parameter set 'SummarySet': parameter 'SummaryExpression' is mandatory." + } + } } \ No newline at end of file diff --git a/src/PrtgAPI.Tests.UnitTests/PowerShell/ObjectManipulation/New-Sensor.Tests.ps1 b/src/PrtgAPI.Tests.UnitTests/PowerShell/ObjectManipulation/New-Sensor.Tests.ps1 index 931b0618..bb8a447a 100644 --- a/src/PrtgAPI.Tests.UnitTests/PowerShell/ObjectManipulation/New-Sensor.Tests.ps1 +++ b/src/PrtgAPI.Tests.UnitTests/PowerShell/ObjectManipulation/New-Sensor.Tests.ps1 @@ -552,6 +552,106 @@ Describe "New-Sensor" -Tag @("PowerShell", "UnitTest") { } } + Context "Factory: HashTable" { + + $sensors = GetSensors + + It "specifies Default" { + + $channelDefinition = @( + "%231%3Adc1%0A" + "channel(4000%2C0)%0A" + "%232%3Adc2%0A" + "channel(4001%2C0)%0A" + "%233%3Adc3%0A" + "channel(4002%2C0)" + ) -join "" + + SetAddressValidatorResponse @( + [Request]::Status() + [Request]::BeginAddSensorQuery(1001, "aggregation") + [Request]::AddSensor("name_=CPU+Overview&priority_=3&inherittriggers_=1&intervalgroup=1&interval_=60%7C60+seconds&errorintervalsdown_=1&tags_=factorysensor&aggregationchannel_=$channelDefinition&warnonerror_=0&aggregationstatus_=&missingdata_=0&sensortype=aggregation&id=1001") + ) + + $sensors | New-Sensor -Factory "CPU Overview" @{name={$_.Device}} -DestinationId 1001 -Resolve:$false + } + + It "specifies Manual" { + + $channelDefinition = @( + "%231%3ALine+at+40.2%0A" + "40.2" + ) -join "" + + SetAddressValidatorResponse @( + [Request]::Status() + [Request]::BeginAddSensorQuery(1001, "aggregation") + [Request]::AddSensor("name_=CPU+Overview&priority_=3&inherittriggers_=1&intervalgroup=1&interval_=60%7C60+seconds&errorintervalsdown_=1&tags_=factorysensor&aggregationchannel_=$channelDefinition&warnonerror_=0&aggregationstatus_=&missingdata_=0&sensortype=aggregation&id=1001") + ) + + $sensors | New-Sensor -Factory "CPU Overview" @{name="Line at 40.2"; value=40.2} -DestinationId 1001 -Resolve:$false + } + + It "specifies Aggregate" { + $channelDefinition = @( + "%231%3AAggregate+Channel%0A" + "channel(4000%2C0)+%2B+channel(4001%2C0)+%2B+channel(4002%2C0)" + ) -join "" + + SetAddressValidatorResponse @( + [Request]::Status() + [Request]::BeginAddSensorQuery(1001, "aggregation") + [Request]::AddSensor("name_=CPU+Overview&priority_=3&inherittriggers_=1&intervalgroup=1&interval_=60%7C60+seconds&errorintervalsdown_=1&tags_=factorysensor&aggregationchannel_=$channelDefinition&warnonerror_=0&aggregationstatus_=&missingdata_=0&sensortype=aggregation&id=1001") + ) + + $sensors | New-Sensor -Factory "CPU Overview" @{name="Aggregate Channel"; aggregator="Sum"} -DestinationId 1001 -Resolve:$false + } + + It "specifies Summary" { + $channelDefinition = @( + "%231%3AAverage+CPU+Usage%0A" + "(channel(4000%2C0)+%2B+channel(4001%2C0)+%2B+channel(4002%2C0))+%2F+3%0A" + "%232%3Adc1%0A" + "channel(4000%2C0)%0A" + "%233%3Adc2%0A" + "channel(4001%2C0)%0A" + "%234%3Adc3%0A" + "channel(4002%2C0)" + ) -join "" + + SetAddressValidatorResponse @( + [Request]::Status() + [Request]::BeginAddSensorQuery(1001, "aggregation") + [Request]::AddSensor("name_=CPU+Overview&priority_=3&inherittriggers_=1&intervalgroup=1&interval_=60%7C60+seconds&errorintervalsdown_=1&tags_=factorysensor&aggregationchannel_=$channelDefinition&warnonerror_=0&aggregationstatus_=&missingdata_=0&sensortype=aggregation&id=1001") + ) + + $sensors | New-Sensor -Factory "CPU Overview" @{ + name={ $_.Device } + sn="Average CPU Usage" + se="Average" + } -DestinationId 1001 -Resolve:$false + } + + It "specifies nested Hashtable" { + $channelDefinition = @( + "%231%3Adc1%0A" + "channel(4000%2C0)%0A" + "%232%3Adc2%0A" + "channel(4001%2C0)%0A" + "%233%3Adc3%0A" + "channel(4002%2C0)" + ) -join "" + + SetAddressValidatorResponse @( + [Request]::Status() + [Request]::BeginAddSensorQuery(1001, "aggregation") + [Request]::AddSensor("name_=CPU+Overview&priority_=3&inherittriggers_=1&intervalgroup=1&interval_=60%7C60+seconds&errorintervalsdown_=1&tags_=factorysensor&aggregationchannel_=$channelDefinition&warnonerror_=0&aggregationstatus_=&missingdata_=0&sensortype=aggregation&id=1001") + ) + + $sensors | New-Sensor -Factory "CPU Overview" @{hashtable=@{name={$_.Device}}} -DestinationId 1001 -Resolve:$false + } + } + Context "Factory: ChannelDefinition" { It "specifies a -ChannelDefinition" { @@ -597,7 +697,7 @@ Describe "New-Sensor" -Tag @("PowerShell", "UnitTest") { It "cannot be used positionally" { SetMultiTypeResponse - { New-Sensor -Factory "CPU Overview" "#1:dc1","channel(4000,0)" -DestinationId 1001 -Resolve:$false } | Should Throw "Cannot convert 'System.Object[]' to the type 'PrtgAPI.PowerShell.NameOrScriptBlock'" + { New-Sensor -Factory "CPU Overview" "#1:dc1","channel(4000,0)" -DestinationId 1001 -Resolve:$false } | Should Throw "Cannot bind parameter 'HashTable'. Cannot convert the `"#1:dc1`" value of type `"System.String`" to type `"System.Collections.Hashtable`"" } It "displays -WhatIf message" { diff --git a/src/PrtgAPI/Linq/EnumerableEx.cs b/src/PrtgAPI/Linq/EnumerableEx.cs index d9e68217..f61f5e7f 100644 --- a/src/PrtgAPI/Linq/EnumerableEx.cs +++ b/src/PrtgAPI/Linq/EnumerableEx.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -105,5 +106,15 @@ internal static ReadOnlyCollection ToReadOnly(this IEnumerable source) return new ReadOnlyCollection(source.ToList()); } + + internal static Dictionary ToDictionary(this Hashtable table) + { + return table.ToDictionary(); + } + + internal static Dictionary ToDictionary(this Hashtable table) + { + return table.Cast().ToDictionary(e => (TKey) e.Key, e => (TValue) e.Value); + } } } diff --git a/src/PrtgAPI/Utilities/ReflectionExtensions.cs b/src/PrtgAPI/Utilities/ReflectionExtensions.cs index f176ae21..cd577bbc 100644 --- a/src/PrtgAPI/Utilities/ReflectionExtensions.cs +++ b/src/PrtgAPI/Utilities/ReflectionExtensions.cs @@ -77,6 +77,16 @@ public static PropertyInfo GetPublicPropertyInfo(this object obj, string name) return prop; } + public static object GetPublicProperty(this object obj, string name) + { + var info = obj.GetPublicPropertyInfo(name); + + if (info == null) + throw new MissingMemberException(obj.GetType().Name, name); + + return info.GetValue(obj); + } + /// /// Retrieve the property info metadata of an internal property. ///