Death by Logging Mantra #1 - Logs Consume Space

§ June 15, 2009 18:03 by beefarino |

As much as I love and lean on logging, it's not immune from problems.  In fact, it can be the source of some serious headaches.  A recent snafu at work prompted me to write about some situations where logging can bring your application performance to a screeching halt, or crash your application altogether. 

Here's what happened...

An incredibly complex system of hardware and software had been running smoothly for months; as part of an instrumentation layer we opted to change the rolling log strategy from 50 10MB text files:

...
<appender name="TextFile" type="log4net.Appender.RollingFileAppender">
    <file value="logs\log.txt" />
    <appendToFile value="true" />
    <rollingStyle value="Size" />
    <maxSizeRollBackups value="50" />
    <maximumFileSize value="10MB" />
    <layout type="log4net.Layout.XMLLayout">
      <prefix value="" />
    </layout>
</appender>
... 

to 500 1MB xml files:

...
<appender name="XmlFile" type="log4net.Appender.RollingFileAppender">
    <file value="logs\log.xml" />
    <appendToFile value="true" />
    <rollingStyle value="Size" />
    <maxSizeRollBackups value="500" />
    <maximumFileSize value="1MB" />
    <staticLogFileName value="false" />
    <countDirection value="1" />
    <layout type="log4net.Layout.XMLLayout">
      <prefix value="" />
    </layout>
</appender>
... 

As an ardent log4net user, I am aware of the performance impact of rolling a large number of files - if the CountDirection setting of the RollingFileAppender is less than 0 (which it is by default), the system will rename every log file each time the log rolls over.  This is costly, and in our product configuration that would mean up to 500 file renames on each roll. 

"Good thing I know what I'm doing...."

Several hours after firing up the new configuration a college asked me to come look at the device.  It had slowed to a wounded crawl.   I went to dig into the logs - I popped over to the log drive and started to find the most recent one.

... but there were 2000 log files, not 500.  The 2GB drive dedicated to the log was completely full.  And the application was still trying to write new log entries, which meant a slew of IO Exceptions were being continuously thrown by multiple threads.

"Um, I think I may have found the problem..."

I removed the oldest 1999 log files and the device immediately recovered.

So what happened?

The configuration XML is 100% correct.  The problem was that I accidentally deployed the software to the device with an old beta version of log4net 1.2.9; that particular version contains a bug in the RollingFileAppender code that prevents the MaxSizeRollBackups from being honored when CountDirection was >= 0.  Without the logs being limited in number, the software eventually filled up the hard disk with log entries.

Which bring me to my first death-by-logging mantra...

Logs Consume Space

It sounds silly I know, but this is the single most prevalent antipattern I see with any logging implementation.  There is a finite amount of storage space, and you need to make sure your logging activity doesn't consume more than its share.

I frequently see this when apps use FileAppender - this beast has no chains and, as I've stated elsewhere, you should never ever use it.  Ever.  Even in "little" applications it can cause massive trouble because the log accumulates over process time or application runs with no checks.  I've seen a 1KB application with 3GB of logs spanning almost a year of activity.

But don't think the problem is limited to the file-based appenders.  Remember....

  • memory is finite...
  • event logs fill up more often than you think...
  • a database can be configured to boundlessly expand as needed...
  • mail servers generally put caps on the size of an inbox...

Whichever appender strategies you choose, you should carefully consider the following:

... How much persistent storage are you allocating to the log?  Your answer should be a firm number, like "500MB", and not "the rest of the disk".  If you can, base this on the amount of information you need to have access to.  If a typical run of your application results in 10MB of logs, you can base the allocated size on the number of runs you want to persist.  If your application runs continuously - like a web site or windows service - you can plot out the logging activity over time, then gauge the allocation size from the time span of activity you want to persist.

... How will you cope when the allocated storage is used up?  Some appenders, like the RollingFileAppender, can handle this for you by freeing up space used by old log entries.  Others, like the EventLogAppender or the AdoNetAppender, blithely log without regard to the amount of space being consumed, and it's up to you to manage the size of the log in other ways.  E.g., I've seen SQL jobs dedicated to removing log records older than N hours, or truncating the log table to the N newest records.

... What happens when you log to a full repository?  Plan for success, but understand the causes of failure.  As I recently learned, our application slows down significantly when the log drive is full, so now checking the free space of the log drive is now included in our troubleshooting guide as a checklist item under "Application Performance Issues".  Test your application under limited logging resources to understand how it will behave.

The most important thing to remember is that logging, like any other subsystem of your application, needs to be planned, tested, and verified.



Creating a PowerShell Provider pt 2: Parameters and Default Drives

§ June 11, 2009 16:26 by beefarino |

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!




Creating a PowerShell Provider pt 1: Creating the Drive

§ June 9, 2009 14:06 by beefarino |

The first step to creating a full-featured PowerShell provider is to be able to create a new drive.  The goal of this post is to enable the most basic functional drive that provides access to the ASP.NET Membership user store.  Before you begin, make sure you have the project set up properly as I describe here, or you can download the project pre-coded (3.24 kb).

We'll start by creating a minimal provider implementation.  Add a class to the project named Provider and fill it in like so:

// Provider.cs
using System.Management.Automation;
using System.Management.Automation.Provider;
namespace ASPNETMembership
{
    [CmdletProvider( "ASPNETMembership", ProviderCapabilities.None )]
    public class Provider : DriveCmdletProvider
    {
        protected override PSDriveInfo NewDrive( PSDriveInfo drive )
        {
            return base.NewDrive( drive );
        }
        protected override object NewDriveDynamicParameters()
        {
            return base.NewDriveDynamicParameters();
        }
    }
}  

A few items of note:

  • the Provider class derives from System.Automation.Provider.DriveCmdletProvider. All PowerShell providers derive from one of the abstract *CmdletProvider base classes. I'll discuss these classes a bit in the next post, but for now know that DriveCmdletProvider is the most basic base class you can use to create a provider.
  • the class is decorated with a System.Management.Automation.Provider.CmdletProviderAttribute.  This attribute is required to get PowerShell to recognize your class as a provider when the assembly is loaded.  In addition, the attribute identifies some of the capabilities the provider supports; more on this in a later post.

The class overrides two methods related to drive creation - at the moment they are simply placeholders and defer to the DriveCmdletProvider implementations.  When PowerShell is presented with a new-psdrive cmdlet specifying the ASPNETMembership provider, it will ferret the call to our provider's NewDrive method to create the drive.

Compile and run; in the PowerShell console that opens, enter the following command (note that my convention is to mark commands you type in with leading >'s):

> new-psdrive -PsProvider ASPNETMembership -Name users -Root "";

This will invoke the NewDrive method of the ASPNETMembership provider and create a new drive named "users" in the PowerShell session.  To verify the drive exists, use the get-psdrive cmdlet.  You should see output similar to the following :

> get-psdrive
Name       Provider      Root                                   CurrentLocation
----       --------      ----                                   ---------------
C          FileSystem    C:\                                 ...ensions\Profile
cert       Certificate   \
Env        Environment
Function   Function
Gac        AssemblyCache Gac
HKCU       Registry      HKEY_CURRENT_USER
HKLM       Registry      HKEY_LOCAL_MACHINE
users      ASPNETMemb...

Custom Drive Objects

Providers are transient: PowerShell will create many instances of this provider in response to different cmdlets.  So it's not possible to persist any state in a provider object.  Instead, state needs to be stored in a custom drive object that is returned from the NewDrive method of the provider.  PowerShell caches this instance and makes it available to the provider via the DriveCmdletProvider.PsDriveInfo property.

The only real requirement for a custom drive object is that it derive from System.Automation.PSDriveInfo.  This class is the most basic implementation of a custom drive object possible; go ahead and add it to the project:

// DriveInfo.cs
using System.Management.Automation;
namespace ASPNETMembership
{
    public class MembershipDriveInfo : PSDriveInfo
    {
        public MembershipDriveInfo( PSDriveInfo driveInfo )
            : base( driveInfo )
        {
        }      
    }
}

Then modify the Provider class to return a new instance of this drive object in the NewDrive method:

// Provider.cs
// ... 
        protected override PSDriveInfo NewDrive( PSDriveInfo drive )
        {
            return new MembershipDriveInfo( drive );
        }
// ...

Build and run; in the console, enter the following commands:

> new-psdrive -psp aspnetmembership -name users -root ""
Name       Provider      Root                                   CurrentLocation
----       --------      ----                                   ---------------
users      ASPNETMemb...
> ( get-psdrive users ).gettype().fullname
ASPNETMembership.MembershipDriveInfo

Note that the type of the users drive is now our custom drive object type - at this point, we have our first soild hook into the powershell provider system.  Now it's time to fold ASP.NET Membership into the custom drive.... but there's a small problem to deal with first.

Configuring ASP.NET Membership Programatically

Configuring ASP.NET Membership in a web application is pretty easy.  You just pop in a little config section into the web.config and everything just works; e.g., this web.config sets up the SqlMembershipProvider for a website:

<configuration>
... 
    <connectionStrings>
        <add name="MembershipProviderServices" 
                 connectionString="data source=localhost;Integrated Security=SSPI;Initial Catalog=AwesomeWebsiteDB"/>
    </connectionStrings>
... 
        <membership>
            <providers>
                <clear/>
                <add name="AspNetSqlMembershipProvider" 
                         type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" 
                         connectionStringName="MembershipProviderServices" 
                         enablePasswordRetrieval="false" 
                         enablePasswordReset="true" 
                         requiresQuestionAndAnswer="false" 
                         requiresUniqueEmail="false" 
                         passwordFormat="Hashed" 
                         maxInvalidPasswordAttempts="5" 
                         minRequiredPasswordLength="6" 
                         minRequiredNonalphanumericCharacters="0" 
                         passwordAttemptWindow="10" 
                         passwordStrengthRegularExpression="" 
                         applicationName="/" />
            </providers>
        </membership>
...
</configuration> 

In PowerShell, there is no equivalent to a web.config or app.config we can use to configure the membership provider.  Instead, we have to configure the memberhip provider via code.  Not a big deal; this short snippit of code results in the same membership provider configuration defined by the web.config example:

var provider = new SqlMembershipProvider();
var nvc = new System.Collections.Specialized.NameValueCollection
{
    { "connectionStringName", "MembershipProviderServices" },
    { "enablePasswordRetrieval", "false" },
    { "enablePasswordReset", "true" },
    { "requiresQuestionAndAnswer", "false" },
    { "requiresUniqueEmail", "false" },
    { "passwordFormat", "Hashed" },
    { "maxInvalidPasswordAttempts", "5" },
    { "minRequiredPasswordLength", "6" },
    { "minRequiredNonalphanumericCharacters", "0" },
    { "passwordAttemptWindow", "10" },
    { "passwordStrengthRegularExpression", "" },
    { "applicationName", "/" },
};
provider.Initialize( "AspNetSqlMembershipProvider", nvc );

The only wrinkle is on line 4, where we reference a connection string by its name.  The ConfigurationManager.ConnectionStrings collection is read-only, so how can we add a new connection string to it?  Simple, we cheat and make the collection writeable (many thanks to David Gardiner for posting a similar solution on his blog!):

using System.Configuration;
var connectionStrings = ConfigurationManager.ConnectionStrings;
// find the private bReadOnly field in the connection strings base type
var fi = typeof( ConfigurationElementCollection );
    .GetField( "bReadOnly", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic );
// change the field value to false, making the object writeable
fi.SetValue( connectionStrings, false );
// add a new connection string
connectionStrings.Add(
    new ConnectionStringSettings(
        "MembershipProviderServices",
        "data source=localhost;Integrated Security=SSPI;Initial Catalog=AwesomeWebsiteDB"
        )
    );

Now with the ability to add connection strings to the configuration manager programmatically, we can add ASP.NET Membership services to our custom drive object.

Adding ASP.NET Membership Features to the Drive

Modify the custom drive class with our ASP.NET Membership configuration code as follows:

// DriveInfo.cs
using System.Configuration;
using System.Management.Automation;
using System.Web.Security;
namespace ASPNETMembership
{
    public class MembershipDriveInfo : PSDriveInfo
    {
        MembershipProvider provider;
        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 );
        }
        
        public MembershipProvider MembershipProvider
        {
            get
            {
                return this.provider;
            }
        }
    }
}

Note that the custom drive holds a reference to the ASP.NET Membership provider that is initialized in the constructor - since PowerShell caches the drive object, it's safe to save the provider reference.  The drive also exposes the membership provider through the public MembershipProvider property.

Build and run.  Now the fun begins - first, let's create get a reference to our drive:

> $d = new-psdrive -P
sProvider ASPNETMembership -Name users -Root ""
> $d.MembershipProvider | get-member
   TypeName: System.Web.Security.SqlMembershipProvider
Name                                 MemberType Definition
----                                 ---------- ----------
ValidatingPassword                   Event      System.Web.Security.Membersh...
ChangePassword                       Method     System.Boolean ChangePasswor...
ChangePasswordQuestionAndAnswer      Method     System.Boolean ChangePasswor...
CreateUser                           Method     System.Web.Security.Membersh...
DeleteUser                           Method     System.Boolean DeleteUser(St...
Equals                               Method     System.Boolean Equals(Object...
FindUsersByEmail                     Method     System.Web.Security.Membersh...
FindUsersByName                      Method     System.Web.Security.Membersh...
GeneratePassword                     Method     System.String GeneratePasswo...
GetAllUsers                          Method     System.Web.Security.Membersh...
GetHashCode                          Method     System.Int32 GetHashCode()
GetNumberOfUsersOnline               Method     System.Int32 GetNumberOfUser...
GetPassword                          Method     System.String GetPassword(St...
GetType                              Method     System.Type GetType()
GetUser                              Method     System.Web.Security.Membersh...
GetUserNameByEmail                   Method     System.String GetUserNameByE...
Initialize                           Method     System.Void Initialize(Strin...
ResetPassword                        Method     System.String ResetPassword(...
ToString                             Method     System.String ToString()
UnlockUser                           Method     System.Boolean UnlockUser(St...
UpdateUser                           Method     System.Void UpdateUser(Membe...
ValidateUser                         Method     System.Boolean ValidateUser(...
ApplicationName                      Property   System.String ApplicationNam...
Description                          Property   System.String Description {g...
EnablePasswordReset                  Property   System.Boolean EnablePasswor...
EnablePasswordRetrieval              Property   System.Boolean EnablePasswor...
MaxInvalidPasswordAttempts           Property   System.Int32 MaxInvalidPassw...
MinRequiredNonAlphanumericCharacters Property   System.Int32 MinRequiredNonA...
MinRequiredPasswordLength            Property   System.Int32 MinRequiredPass...
Name                                 Property   System.String Name {get;}
PasswordAttemptWindow                Property   System.Int32 PasswordAttempt...
PasswordFormat                       Property   System.Web.Security.Membersh...
PasswordStrengthRegularExpression    Property   System.String PasswordStreng...
RequiresQuestionAndAnswer            Property   System.Boolean RequiresQuest...
RequiresUniqueEmail                  Property   System.Boolean RequiresUniqu...

As you can see, the membership provider is fully accessible from PowerShell via the drive object.  We can create users:

> $status = 0;
> $d.MembershipProvider.CreateUser( "myuser", "mypassword1234", "myuser@hotmail.com", "what is your favorite color?", "plaid", $true, [Guid]::NewGuid(), ([ref]$status))
UserName                : myuser
ProviderUserKey         : b6ebc2a4-2ff8-4cf8-928e-969416edd704
Email                   : myuser@hotmail.com
PasswordQuestion        : what is your favorite color?
Comment                 :
IsApproved              : True
IsLockedOut             : False
LastLockoutDate         : 1/1/1754 12:00:00 AM
CreationDate            : 6/11/2009 1:13:50 PM
LastLoginDate           : 6/11/2009 1:13:50 PM
LastActivityDate        : 6/11/2009 1:13:50 PM
LastPasswordChangedDate : 6/11/2009 1:13:50 PM
IsOnline                : True
ProviderName            : AspNetSqlMembershipProvider

We can get existing users:

> $d.MembershipProvider.GetUser( "myuser", $false )
UserName                : myuser
ProviderUserKey         : b6ebc2a4-2ff8-4cf8-928e-969416edd704
Email                   : myuser@hotmail.com
PasswordQuestion        : what is your favorite color?
Comment                 :
IsApproved              : True
IsLockedOut             : False
LastLockoutDate         : 12/31/1753 7:00:00 PM
CreationDate            : 6/11/2009 1:13:50 PM
LastLoginDate           : 6/11/2009 1:13:50 PM
LastActivityDate        : 6/11/2009 1:13:50 PM
LastPasswordChangedDate : 6/11/2009 1:13:50 PM
IsOnline                : True
ProviderName            : AspNetSqlMembershipProvider 

We can even remove users:

> $d.MembershipProvider.DeleteUser( "myuser", $true )
True
> $d.MembershipProvider.GetUser( "myuser",$false ) 
#note that no output is returned this time 

Using the drive directly to manage users may work, but it is a bit clumsy.  You may be asking yourself why we implemented the drive at all, given that PowerShell could create the MembershipProvider object directly.  The drive is the basis on which all PowerShell provider functionality will be based; many standard cmdlets leverage the data stored in a drive object.  So things may look cumbersome at the moment, but soon we'll be working with cmdlets, pipes, and sets of user objects - all thanks to our custom drive object.

Coming Up

At the moment, the ASP.NET Membership provider configuration lives code; that should really change so this drive can be used with other providers or for multiple sites.  The NewDriveDynamicParameters method of the PowerShell provider object allows the provider to request additional drive creation parameters from the new-psdrive cmdlet.  My next post will describe how to properly implement this method.

In upcoming posts, I'll show you how to implement support for different core cmdlets, such as get-item, remove-item, new-item, and get-childitem (or 'dir' for you alias users).  In addition, I'll dig deep into the various path syntaxes that PowerShell expects your provider to support. 

The code for this post is available here:

ASPNETMembership_pt1.zip (3.24 kb)

As always, thanks for reading, and if you liked this post, please Kick It, Shout It, trackback, or comment.



Log4Net Tutorials and Resources

§ June 7, 2009 03:57 by beefarino |

I've been meaning to create a landing page for all the log4net traffic on my blog.  A friend of mine recently told me how difficult it is to drill through all the tutorials, so I decided to organize access to all the log4net stuff in one place.  I'll keep this page updated as the blog grows. 

If there are any log4net resources you think I should add to this list or blog about, please add them in the comments or send them to me via the contact link.

Log4Net Tutorials

If you've never used log4net before, you should begin with the getting started and configuration topics.  You'll want to review the appender, layout, and patterns tutorials next, and use the others as you see fit.

  • Getting Started with Log4Net - never used log4net before, but need to get running quickly?
  • Basic XML Configuration - describes how to isolate log4net configuration to an XML file for easier modification.
  • Appenders - a discussion of the catalog of appenders available in the log4net distribution. Need to figure out how to log to the console? Or a text file? Or a database? This tutorial is for you.
  • Layouts and Patterns - want more control over the way your log looks?
  • Using Logger Objects - learn to use logger objects to organize your logging code into independently configurable parts.
  • Log Event Context - need some extra data in that log message? Maybe a call stack label, user identifier, or session moniker?
  • Filters - learn how to tame that log output and filter out the stuff you don't want or need.
  • Lossy Logging - learn about an advanced technique of optimizing your logging activity. Lossy logging gives you the benefit of a verbose log with the performance of a minimal logging scenario.

Recomended Practices

  • Your Code - learn what you should do in your code to make the most of logging.
  • Isolating Bugs - see how logging can be used to isolate production bugs.

Death by Logging Mantras

These mantras discuss specific scenarios where logging can cripple your application or its performance.  These are in-process and links will become available as I write the posts.

Resources