Is my understanding of PSPropertyAdapter correct?

Viewed 66

I am writing a binary PowerShell module that, much like the ActiveDirectory module, will have a number of cmdlets and types that can potentially return more than the default set of properties - and those properties (100+) are dependent on what the user requests. Just like the AD module, I would like to return the type with the properties that were requested so that it is transparent to the user i.e. not one huge class with lots of empty properties that they haven't requested.

I was looking at the documentation for the ActiveDirectory module and I noticed that things like ADUser and ADComputer ultimately are inherited from ADPropertyCollection which I assume has the properties required in its InnerDictionary after whatever searches it does.

This did not really explain how PowerShell presents the AD types flexibly i.e.

(Get-ADUser -Identity some.user).GetType()
(Get-ADUser -Identity some.user -Properties *).GetType()
# Both return a type of ADUser, despite have drastically different amounts of properties.

This was until I looked at the Types ps1xml file for the AD module, which presents something like this:

<Types>
    <!-- other types -->
     <Type>
        <Name>Microsoft.ActiveDirectory.Management.ADUser</Name>
        <TypeAdapter>
            <TypeName>Microsoft.ActiveDirectory.Management.ADEntityAdapter</TypeName>
        </TypeAdapter>
    </Type>
</Types>

So I guess the source of the "magic" properties is ADEntityAdapter which is inherited from the abstract class PSPropertyAdapter.

The issue I have is now I am not sure how to implement it and there are not any easily searchable examples of it being implemented. I appreciate its a real edge case. I have had a small attempt at a very rough implementation below, please ignore any code faux pas - I will of course not actually write the code like below. I just wanted to at least try and show I have thought about this.

using System.Collections.Generic;

namespace Sample
{
    public class PropertyCollection
    {
        public string Id { get; set; }

        public Dictionary<string, object> Attributes { get; set; }

        public PropertyCollection()
        {
            this.Id = "SampleID";
            this.Attributes = new Dictionary<string, object>();

            for (var i = 0; i < 10; i++)
            {
                this.Attributes.Add("KeyAttribute" + i, i);
            }

            for (var i = 10; i < 100; i++)
            {
                this.Attributes.Add("OtherAttribute" + i, i);
            }
        }
    }
}
namespace Sample
{
    public class ReturnedClass : PropertyCollection
    {
        public string SomeName { get; set; }

        public ReturnedClass() : base() { }
    }
}
using System;
using System.Collections.ObjectModel;
using System.Management.Automation;

namespace Sample
{
    public class PropertyEntityAdapter : PSPropertyAdapter
    {
        public override Collection<PSAdaptedProperty> GetProperties(object baseObject)
        {
            PropertyCollection pc = baseObject as PropertyCollection;
            if (pc == null)
            {
                throw new Exception("Some Exception");
            }
            Collection<PSAdaptedProperty> collection = new Collection<PSAdaptedProperty>();
            foreach (string name in pc.Attributes.Keys)
            {
                collection.Add(new PSAdaptedProperty(name, null));
            }
            return collection;
        }

        public override PSAdaptedProperty GetProperty(object baseObject, string propertyName)
        {
            PropertyCollection pc = baseObject as PropertyCollection;
            if (pc.Attributes.TryGetValue(propertyName, out object pcValue))
            {
                return new PSAdaptedProperty(propertyName, pcValue);
            }
            throw new Exception("Prop not found");
        }

        public override string GetPropertyTypeName(PSAdaptedProperty adaptedProperty)
        {
            if (adaptedProperty == null)
            {
                throw new Exception("prop null");
            }
            PropertyCollection pc = adaptedProperty.BaseObject as PropertyCollection;
            if (pc == null)
            {
                throw new Exception("not found");
            }
            if (pc.Attributes.TryGetValue(adaptedProperty.Name, out object pcValue))
            {
                return pcValue.GetType().FullName;
            }
            return pc.GetType().FullName;
        }

        public override object GetPropertyValue(PSAdaptedProperty adaptedProperty)
        {
            if (adaptedProperty?.BaseObject == null || adaptedProperty?.Name == null)
            {
                throw new Exception("prop null");
            }
            PropertyCollection pc = adaptedProperty.BaseObject as PropertyCollection;
            if (pc == null)
            {
                throw new Exception("not found");
            }
            if (pc.Attributes.TryGetValue(adaptedProperty.Name, out object pcValue))
            {
                return pcValue;
            }
            return null;
        }

        public override bool IsGettable(PSAdaptedProperty adaptedProperty)
        {
            if (adaptedProperty == null)
            {
                throw new Exception("prop null");
            }
            PropertyCollection pc = adaptedProperty.BaseObject as PropertyCollection;
            if (pc == null)
            {
                throw new Exception("not found");
            }
            if (pc.Attributes.ContainsKey(adaptedProperty.Name))
            {
                return true;
            }
            return false;
        }

        public override bool IsSettable(PSAdaptedProperty adaptedProperty)
        {
            return false;
        }

        public override void SetPropertyValue(PSAdaptedProperty adaptedProperty, object value)
        {
            throw new System.NotImplementedException();
        }
    }
}

** I have purposely not added any code for the Set* methods to reduce the code required here. If the above seems right, then I am sure I know what to do.

Has anyone done this before? Are there any very basic examples out there? Am I on the right path, or am I misunderstanding? Thanks in advance.

1 Answers

If anyone comes comes looking for this and was interested in doing something similar, then my question is actually more or less how it is done. I hope it is of use or interest to others working on binary modules.

One thing I did find were there were some additional properties that were passed through the GetProperty method that seemed to be from the PowerShell engine itself i.e. PSComputerName, so there was no need to throw an error here, just return a new PSAdaptedProperty with a null object.

public override PSAdaptedProperty GetProperty(object baseObject, string propertyName)
{
    PropertyCollection pc = baseObject as PropertyCollection;
    if (pc.Attributes.TryGetValue(propertyName, out object pcValue))
    {
        return new PSAdaptedProperty(propertyName, pcValue);
    }
    return new PSAdaptedProperty(propertyName, null);
}

You can then either load the Types data via your module manifest or in the console using Update-Typedata -PrependPath path_to_file\file_name.

All the properties in the dictionary are returned to give the appearance that they are full properties of the class.

Related