Creating custom powershell cmdlet
By Anatoly Mironov
Why
I need to to activate a feature in PowerShell and specify some properties. Simple? Yes. Possible? No. In the default Enable-SPFeature cmdlet you can’t specify any properties:
Enable-SPFeature –Identity "b5eef7d1-f46f-44d1-b53e-410f62032846" -URL http://dev
We can of course easily add properties when activating features in onet.xml:
<!-- Publishing Resources -->
<Feature ID="AEBC918D-B20F-4a11-A1DB-9ED84D79C87E">
<Properties xmlns="http://schemas.microsoft.com/sharepoint/">
<Property Key="AllowRss" Value="false" />
<Property Key="SimplePublishing" Value="false" />
</Properties>
</Feature>
So I went to SharePoint StackExchange and asked the question. Then I realized: the standard Sharepoin API doesn’t support this neither. The methods of SPFeatureCollection which have SPFeatureProperties as parameter are internal. So the only way is to use Reflection like Hristo Pavlov (2008) and Yaroslav Pentsarsky (2010) suggest. So why not to try to create a cmdlet?
What
I want to create a custom cmdlet: Enable-SPFeatureWithProperties. It should look like this: [sourcecode language=“powershell”]$properties = @{“Test” = “SPLENDID” } Enable-SPFeatureWithProperties ` –Identity “b5eef7d1-f46f-44d1-b53e-410f62032846” ` -URL http://dev ` -Properties $properties[/sourcecode] The cmdlet code will be written in C# and published on github. It is a part of my project called sp-lend-id and like all other parts in sp-lend-id it will have a Chuvash word as its name: Taprat (/taprat/ meaning “Enable” :) )
How
First I create a simple cmdlet just to get started. Then I test the reflection code to activate a feature with properties. Then, if this works, I will bring the pieces together and create my cmdlet.
Simple cmdlet
The best cmdlet tutorial (text, images, code and videocast) is created by Saveen Reddy. I just followed his sample. I created a new class library project and a class for the demo cmdlet: Get_DemoNames.cs. By the way, if you don’t have th powershell dll as Saveen Reddy describes, add manually the reference to the csproj-file:
<Reference Include="System.Management.Automation" />
Activating the feature
Create a very simple Feature and Feature Receiver which takes properties and does something with them, like Yaroslav suggested:
public override void FeatureActivated(SPFeatureReceiverProperties properties) { var web = properties.Feature.Parent as SPWeb; if (web != null) { var allow = web.AllowUnsafeUpdates; web.AllowUnsafeUpdates = true; if (properties.Feature.Properties\["Test"\] != null) { web.Title = properties.Feature.Properties\["Test"\].Value; web.Update(); } web.AllowUnsafeUpdates = allow; } }\[/sourcecode\] After deploying this feature we can create a simple console application to test the reflection code. First, the extension for SPFeature to be able to invoke the internal methods: \[sourcecode language="csharp"\]public static class SPFeatureExtensions { public static SPFeature ActivateFeature(this SPFeatureCollection features, Guid featureId, Dictionary<string, string> activationProps) { var propCollConstr = typeof(SPFeaturePropertyCollection).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)\[0\]; var properties = (SPFeaturePropertyCollection) propCollConstr.Invoke(new object\[\] { null }); foreach (var key in activationProps.Keys) { properties.Add(new SPFeatureProperty(key, activationProps\[key\])); } return ActivateFeature(features, featureId, properties); } private static SPFeature ActivateFeature(this SPFeatureCollection features, Guid featureId, SPFeaturePropertyCollection properties) { if (features\[featureId\] != null) { // The feature is already activated. No action required return null; } var type = typeof(SPFeatureCollection); // now we have to get "AddInternal" Method with this signature: //internal SPFeature AddInternal(Guid featureId, Version version, SPFeaturePropertyCollection properties, bool force, bool fMarkOnly, SPFeatureDefinitionScope featdefScope) var param = new\[\] { typeof (Guid), typeof (Version), typeof (SPFeaturePropertyCollection), typeof (bool), typeof (bool), typeof (SPFeatureDefinitionScope) }; var addInternal = type.GetMethod("AddInternal", BindingFlags.Instance | BindingFlags.NonPublic, null, param, null); if (addInternal == null) { // failed to find the method return null; } var result = addInternal.Invoke(features, new object\[\] { featureId, null, properties, false, false, SPFeatureDefinitionScope.Farm }); return result as SPFeature; } }
There are some changes compared to Yaroslav’s code:
- The testing if feature is enabled happens without internal Get Method, just by (features[featureId] != null)
- The internal “Add” method now is called “AddInternal”
- The parameters which have to be passed to the internal add method are changed, too.
I found the signature of the “AddInternal” using Reflector.NET 6: [sourcecode language=“csharp”]internal SPFeature AddInternal(Guid featureId, Version version, SPFeaturePropertyCollection properties, bool force, bool fMarkOnly, SPFeatureDefinitionScope featdefScope) { return this.AddInternalWithName(featureId, null, version, properties, force, fMarkOnly, featdefScope); }[/sourcecode]
Testing the code in a console application
Just a few lines of code to test and debug the code:
static void Main(string[] args) { using (var site = new SPSite("http://dev")) { using (var web = site.OpenWeb()) { var activationProps = new Dictionary {{"Test", "Title changed by feature in activation"}}; web.Features.ActivateFeature( new Guid("b5eef7d1-f46f-44d1-b53e-410f62032846"), activationProps); } } }
The pre-condition is that the test feature is deployed, then it works:
Running it in Powershell
Now, when we know the code works, we can test it in powershell. We create three params: [sourcecode language=“powershell”][System.Management.Automation.Parameter(Position = 0, Mandatory = true)] public string Identity; [System.Management.Automation.Parameter(Position = 1, Mandatory = true)] public string Url; [System.Management.Automation.Parameter(Position = 2, Mandatory = false)] public System.Collections.Hashtable Properties;[/sourcecode] The third parameter is Hashtable because the main “dictionary” object in PowerShell is Hashtable. So we must convert the passed hashtable into a Dictionary:
private Dictionary<string, string> GetProperties()
{
var dictionary = new Dictionary<string, string>();
if (Properties != null)
{
foreach (var key in Properties.Keys)
{
dictionary.Add((string)key, (string)Properties\[key\]);
}
}
return dictionary;
}
Then the only thing we have to do is to copy the code from the Main method in console app into the ProcessRecord method:
protected override void ProcessRecord() { var properties = GetProperties(); WriteObject(properties); using (var site = new SPSite(Url)) { using (var web = site.OpenWeb()) { var features = web.Features; var id = new Guid(Identity); features.ActivateFeature(id, properties); } } }
Now we can rebuild the solution and run the powershell by importing the module:
$p = @{"Test" = "Tjena" } Enable-SPFeatureWithProperties -Identity "b5eef7d1-f46f-44d1-b53e-410f62032846" -Url "http://dev" -Properties $p
Creating PSSnapin
So the cmdlet is completed. There is one thing left which I want to try: creating pssnapin which one can add in an easy way to a powershell session. To create a pssnapin we have to create class in the project and extend it from PSSnapin. This class just has some properties: Name, Description and Vendor: [sourcecode language=“csharp”][RunInstaller(true)] public class TapratInstaller : PSSnapIn { public override string Name { get { return “sp-lend-id.taprat”; } } public override string Vendor { get { return “Anatoly Mironov”; } } public override string Description { get { return “This includes an experimental cmdlet to activate sharepoint features and specify custom properties”; } } }[/sourcecode] To install the pssnapin, run these commands in powershell as Administrator: [sourcecode language=“powershell”]Set-Alias installutil $env:windir\Microsoft.NET\Framework64\v2.0.50727\installutil.exe installutil C:\code\sp-lend-id\taprat\cmdlet\sp-lend-id.taprat\bin\Debug\sp-lend-id.taprat.dll[/sourcecode] Pay attention that we have to run installutil.exe from Framework64, not Framework. Otherwise the pssnapin won’t be accessible. After the pssnapin has been installed we can see it by running:
Get-PSSnapin -registered
Now all we have to is to add the newly created pssnapin and run the commands: [sourcecode language=“powershell”]Add-PSSnapin sp-lend-id.taprat Get-DemoNames -Prefix “Hello “[/sourcecode] And the desired: [sourcecode language=“powershell”]$properties = @{“Test” = “SPLENDID” } Enable-SPFeatureWithProperties ` –Identity “b5eef7d1-f46f-44d1-b53e-410f62032846” ` -URL http://dev ` -Properties $properties[/sourcecode] Of course, in order this to work, we have to deactivate our test feature first. Then the cmdlet activates our test feature which only updates web title in this experiment. It works. Splendid!
Code
The code can be found on sp-lend-id project’s code repository on github: https://github.com/mirontoli/sp-lend-id
DISCLAIMER:
This code is just a lab code for experimenting and learning purposes. Don’t use it in production environments unless you test it yourself. As Per Jacobsen pointed in his answer on Sharepoint StackExchange, the internal methods can vary in different releases and patches of SharePoint, which we can see here (Yaroslav’s code can’t be run anymore), and it can break your solution deployment.
Comments from Wordpress.com
Suman - May 2, 2013
You are the man…!! The line “Pay attention that we have to run installutil.exe from Framework64, not Framework. Otherwise the pssnapin won’t be accessible.” from your blog made my day. I was struggling since a while and finally after reading here, I was able to access it
Anatoly Mironov - May 3, 2013
Hi Suman. Thank you for your feedback. I understand your joy when things work.