My previous post in this series discussed the first major step in developing a PowerShell provider: creating a custom drive object. This post rounds out the discussion by making the drive configurable with custom initialization parameters. In addition, I'll demonstrate how a provider can create one or more "default" drives without user intervention.
This post builds on code from the previous post, which you can download from here. The completed code for this post is also available.
Parameterizing the Drive
At the moment, the ASP.NET Membership provider configuration lives
in code; that should really change so this drive can be used with other
providers or for multiple sites. Such drive configurability is gained by supporting the NewDriveDynamicParameters method of the PowerShell provider.
The NewDriveDynamicParameters method
is called each time the new-psdrive cmdlet is invoked to allow the provider to add drive creation parameters. Although the MSDN documentation of this method isn't terribly helpful, implementing it is pretty simple. All you need to do is return an object containing properties or fields for each custom parameter.
First, we need to know what parameters are needed to create the ASP.NET Membership provider. Take a quick look at the configuration items hard-coded in the drive creation logic:
// DriveInfo.cs
public MembershipDriveInfo( PSDriveInfo driveInfo )
: base( driveInfo )
{
var connectionStrings = ConfigurationManager.ConnectionStrings;
var fi = typeof( ConfigurationElementCollection )
.GetField( "bReadOnly", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic );
fi.SetValue( connectionStrings, false );
connectionStrings.Add(
new ConnectionStringSettings(
"ProviderConnectionString",
"data source=localhost;Integrated Security=SSPI;Initial Catalog=AwesomeWebsiteDB"
)
);
provider = new SqlMembershipProvider();
var nvc = new System.Collections.Specialized.NameValueCollection
{
{ "connectionStringName", "ProviderConnectionString" },
{ "enablePasswordRetrieval", "false" },
{ "enablePasswordReset", "true" },
{ "requiresQuestionAndAnswer", "false" },
{ "requiresUniqueEmail", "false" },
{ "passwordFormat", "Hashed" },
{ "maxInvalidPasswordAttempts", "5" },
{ "minRequiredPasswordLength", "6" },
{ "minRequiredNonalphanumericCharacters", "0" },
{ "passwordAttemptWindow", "10" },
{ "passwordStrengthRegularExpression", "" },
{ "applicationName", "/" },
};
provider.Initialize( "AspNetSqlMembershipProvider", nvc );
}
There are a number of parameters used to initialize the ASP.NET membership provider. Based on the code, I would want to configure any of the following facets of the ASP.NET Membership provider:
- the Connection String server and databse name;
- EnablePasswordRetrieval;
- EnablePasswordReset;
- RequiresQuestionAndAnswer;
- RequiresUniqueEmail;
- PasswordFormat;
- MaxInvalidPassordAttempts;
- MinRequiredPasswordLength;
- MinRequiredNonalphanumericCharacters;
- PasswordAttemptWindow;
- PasswordStrengthRegularExpression;
- ApplicationName.
While all of these should be configurable, assumptions can be made
for most of them.
The first step towards implementing the NewDriveDynamicParameters method is to define an object containing read/write properties for each configurable parameter:
// DriveParameters.cs
using System;
using System.Management.Automation;
using System.Web.Security;
namespace ASPNETMembership
{
public class DriveParameters
{
public DriveParameters()
{
EnablePasswordReset = true;
EnablePasswordRetrieval = false;
RequiresQuestionAndAnswer = false;
RequiresUniqueEmail = false;
MaxInvalidPasswordAttempts = 5;
MinRequiredNonalphanumericCharacters = 0;
MinRequiredPasswordLength = 6;
PasswordAttemptWindow = 10;
PasswordStrengthRegularExpression = String.Empty;
ApplicationName = "/";
PasswordFormat = MembershipPasswordFormat.Hashed;
}
[Parameter( Mandatory=true )]
public string Server { get; set; }
[Parameter( Mandatory = true )]
public string Catalog { get; set; }
public bool EnablePasswordRetrieval { get; set; }
public bool EnablePasswordReset { get; set; }
public bool RequiresQuestionAndAnswer { get; set; }
public bool RequiresUniqueEmail { get; set; }
public MembershipPasswordFormat PasswordFormat { get; set; }
public int MaxInvalidPasswordAttempts { get; set; }
public int MinRequiredPasswordLength { get; set; }
public int MinRequiredNonalphanumericCharacters { get; set; }
public int PasswordAttemptWindow { get; set; }
public string PasswordStrengthRegularExpression { get; set; }
public string ApplicationName { get; set; }
}
}
Note the use of the System.Management.Automation.ParameterAttribute on lines 24 and 27 to identify the Server and Catalog parameters as required (Mandatory=true). The remaining optional parameters are initialized to default values in the constructor.
With the DriveParameters type defined, the NewDriveDyanmicParameters method of the ASP.NET Membership provider can now be implemented:
// Provider.cs
protected override object NewDriveDynamicParameters()
{
return new DriveParameters();
}
PowerShell will call this method each time the new-psdrive cmdlet is invoked for this provider. The object returned to PowerShell is filled with parameter values and passed back to the provider's NewDrive method via the DynamicParameters property; in the Membership PowerShell provider, NewDrive simply passes the parameters to the MembershipDriveInfo constructor:
// Provider.cs
protected override System.Management.Automation.PSDriveInfo NewDrive( PSDriveInfo drive )
{
var driveParams = this.DynamicParameters as DriveParameters;
return new MembershipDriveInfo(drive, driveParams);
}
which must then act on the parameters when initializing the ASP.NET Membership provider:
// DriveInfo.cs
using System;
using System.Configuration;
using System.Management.Automation;
using System.Web.Security;
namespace ASPNETMembership
{
public class MembershipDriveInfo : PSDriveInfo
{
MembershipProvider provider;
public MembershipDriveInfo( PSDriveInfo driveInfo, DriveParameters parameters )
: base( driveInfo )
{
var connectionStrings = ConfigurationManager.ConnectionStrings;
var fi = typeof( ConfigurationElementCollection )
.GetField( "bReadOnly", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic );
fi.SetValue( connectionStrings, false );
var connectionString = String.Format(
"data source={0};Integrated Security=SSPI;Initial Catalog={1}",
parameters.Server,
parameters.Catalog
);
var moniker = Guid.NewGuid().ToString("N");
var connectionStringName = "cxn_" + moniker;
var providerName = "pvdr_" + moniker;
connectionStrings.Add(
new ConnectionStringSettings(
connectionStringName,
connectionString
)
);
provider = new SqlMembershipProvider();
var nvc = new System.Collections.Specialized.NameValueCollection
{
{ "connectionStringName", connectionStringName },
{ "enablePasswordRetrieval", parameters.EnablePasswordRetrieval.ToString() },
{ "enablePasswordReset", parameters.EnablePasswordReset.ToString() },
{ "requiresQuestionAndAnswer", parameters.RequiresQuestionAndAnswer.ToString() },
{ "requiresUniqueEmail", parameters.RequiresUniqueEmail.ToString() },
{ "passwordFormat", parameters.PasswordFormat.ToString() },
{ "maxInvalidPasswordAttempts", parameters.MaxInvalidPasswordAttempts.ToString() },
{ "minRequiredPasswordLength", parameters.MinRequiredPasswordLength.ToString() },
{ "minRequiredNonalphanumericCharacters", parameters.MinRequiredNonalphanumericCharacters.ToString() },
{ "passwordAttemptWindow", parameters.PasswordAttemptWindow.ToString() },
{ "passwordStrengthRegularExpression", parameters.PasswordStrengthRegularExpression },
{ "applicationName", parameters.ApplicationName },
};
provider.Initialize( providerName, nvc );
}
public MembershipProvider MembershipProvider
{
get
{
return this.provider;
}
}
}
}
Not a lot of new code here; the constructor now accepts an additional DriveParameters argument that is used to initialize the connection string and membership provider configuration. Using dynamically-generated connection string and provider names prevents naming collisions if more than one drive is initialized by the provider (lines 26-28).
Using the Drive Parameters
Build and run. The new dynamic drive parameters can be specified directly to the new-psdrive cmdlet just like any other cmdlet parameter:
> new-psdrive -psprovider aspnetmembership -name users -root ''
-server localhost -catalog AwesomeWebsiteDB
-MinRequiredPasswordLength 8 -PasswordAttemptWindow 4
Name Provider Root CurrentLocation
---- -------- ---- ---------------
users ASPNETMemb...
Note that PowerShell prompts the user for the any required parameters if they aren't supplied:
> remove-psdrive users
> new-psdrive -psprovider aspnetmembership -name users -root ''
cmdlet New-PSDrive at command pipeline position 1
Supply values for the following parameters:
Catalog:
> AwesomeWebsiteDB
Server:
> localhost
Name Provider Root CurrentLocation
---- -------- ---- ---------------
users ASPNETMemb...
Creating Default Drives
Another feature available to PowerShell providers is the ability to initialize a set of default drives when the provider assembly is first loaded. This is accomplished by overriding the provider's InitializeDefaultDrives method to return a list of PSDriveInfo objects describing the drives to create:
// Provider.cs
protected override Collection<PSDriveInfo> InitializeDefaultDrives()
{
var driveInfo = new PSDriveInfo(
"users",
this.ProviderInfo,
"",
"Default ASP.NET Membership Drive",
null
);
var parameters = new DriveParameters
{
Catalog = "AwesomeWebsiteDB",
Server = "localhost"
};
return new Collection<PSDriveInfo>
{
new MembershipDriveInfo(
driveInfo,
parameters
)
};
}
Each PSDriveInfo object in the returned collection will then be passed to the NewDrive method of the provider to complete the drive creation (the NewDriveDynamicParameters method is not called in this case). As such, the ASP.NET Membership PowerShell provider's NewDrive method must be modified to acommodate pre-initialized drives:
protected override PSDriveInfo NewDrive( PSDriveInfo drive )
{
// see if the drive has already been initialized
// e.g., via InitializeDefaultDrives()
if( drive is MembershipDriveInfo )
{
return drive;
}
var driveParams = this.DynamicParameters as DriveParameters;
return new MembershipDriveInfo(drive, driveParams);
}
Testing Default Drive Creation
Build and run; you can verify that the default drive was created using the get-psdrive cmdlet:
> get-psdrive
Name Provider Root CurrentLocation
---- -------- ---- ---------------
...
users ASPNETMemb...
No need to use the new-psdrive cmdlet - the users drive is created automagically as PowerShell loads the provider assembly.
Coming Up
That's about it for drives; for here on in, the focus will be on making the provider work with all the features available in PowerShell. The next post will add support for the get-item cmdlet and discuss the various flavors of item paths the provider must support.
The code for this post is available here:
ASPNETMembership_pt1_drives.zip (4.26 kb)
As always, thanks for reading, and if you liked this post, please Kick It, Shout It, trackback, tweet it, and comment using the clicky thingies below!