Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Asp Net 2.0 Security Membership And Role Management

.pdf
Скачиваний:
51
Добавлен:
17.08.2013
Размер:
12.33 Mб
Скачать

Chapter 9

using System;

using System.Configuration.Provider;

namespace SampleFeature

{

public class SampleFeatureProviderCollection : ProviderCollection

{

public override void Add(ProviderBase provider)

{

if (provider == null)

throw new ArgumentNullException(

“You must supply a provider reference”);

if (!(provider is SampleFeatureProvider)) throw new ArgumentException(

“The supplied provider type must derive from SampleFeatureProvider”);

base.Add(provider);

}

new public SampleFeatureProvider this[string name]

{

get { return (SampleFeatureProvider)base[name]; }

}

public void CopyTo(SampleFeatureProvider[] array, int index)

{

base.CopyTo(array, index);

}

}

}

As you can see, a provider collection class is pretty much boilerplate code. The override for the Add method has some extra validation logic to ensure that only instances of SampleFeatureProvider are added to the collection. The default indexer and the CopyTo implementations simply cast the provider reference returned by the underlying ProviderCollection to a SampleFeatureProvider reference.

The public portion of the sample feature is accessible through a static entry class called Sample FeatureMainEntryPoint. This design mirrors the approach used by many of the ASP.NET 2.0 provider-based features. The class definition below shows the relevant portions used for the public API.

using System;

using System.Configuration;

using System.Configuration.Provider; using System.Web.Configuration;

namespace SampleFeature

{

public static class SampleFeatureMainEntryPoint

{

//Initialization related variables and logic //snip...

//Public feature API

352

The Provider Model

private static SampleFeatureProvider defaultProvider;

private static SampleFeatureProviderCollection providerCollection;

public static SampleFeatureProvider Provider

{

get

{

return defaultProvider;

}

}

public static SampleFeatureProviderCollection Providers

{

get

{

return providerCollection;

}

}

public static string GetMeAString(string someString)

{

return Provider.GetMeAString(someString);

}

}

}

The static feature class allows you to access its default provider via the Provider property. If you configure multiple providers with the feature, you can choose a specific provider with the corresponding Providers property. Last, the static feature class exposes the functionality that is implemented by way of a provider. This sample intentionally has a simplistic piece of logic; you can ask the feature for a string, and it will return a string from the default provider. Complex provider-based features like Membership have a hefty number of static feature methods providing a variety of overloads that map to methods in the underlying providers.

A provider-based feature can be considered to go through a lifecycle of sorts:

1.First the feature is in an uninitialized state. Any call to a method on the static feature class should result in initialization.

2.If initialization succeeds, the feature is considered initialized.

3.If initialization failed, the feature can still be considered initialized, but in a failed state. The fact that initialization failed needs to be stored somewhere.

So, a side effect of the feature’s initialization should either be a functioning static class, or some persistent representation of the initialization failure. The sample feature’s private Initialize method is written to throw an exception if initialization failed. As a result, any attempt to call a public property or method on the SampleFeatureMainEntryPoint class results in an exception if initialization failed. More specifically, any attempt to call a public static method or property will fail with an exception stating that the type initializer failed. If you then drill into the InnerException, you will see the specific details of what caused the failure.

353

Chapter 9

Because the initialization process for the feature is the place where configuration and providers come together, let’s take a look at the initialization related code for the static feature class.

public static class SampleFeatureMainEntryPoint

{

//Initialization related variables and logic private static bool isInitialized = false; private static Exception initializationException;

private static object initializationLock = new object();

static SampleFeatureMainEntryPoint()

{

Initialize();

}

private static void Initialize()

{

///implementation

}

}

The feature class holds its initialization state inside of two private variables. If the initialization process has occurred, regardless of its success, then isInitialized will be set to true. If the initialization process failed, an exception has occurred, and this exception will be cached for the lifetime of the application, using the initializationException variable. Both variables are static because the initialization process itself is triggered by the feature class’s static constructor.

Because the Framework calls the type’s static constructor before running any public properties and methods call, the very first call to any portion of the public API will cause the Initialize method to carry out the necessary initialization work. This is the one point where a call to Initialize will actually result in feature initialization. The actual logic within the Initialize method is shown here:

private static void Initialize()

{

//If for some reason the feature has already initialized //then exit, or optionally throw if init failed

if (isInitialized)

{

if (initializationException != null) throw initializationException;

else

return;

}

//Start the initialization lock (initializationLock)

{

//Need to double-check after the lock was taken if (isInitialized)

{

if (initializationException != null) throw initializationException;

else

return;

354

The Provider Model

}

try

{

//Get the feature’s configuration info SampleFeatureConfigurationSection sc = (SampleFeatureConfigurationSection)

ConfigurationManager.GetSection(“sampleFeature”);

if (sc.DefaultProvider == null ||

sc.Providers == null || sc.Providers.Count < 1)

throw new ProviderException(“The feature requires that you “ + “ specify a default “ +

“feature provider as well as at least one “ + “provider definition.”);

//Instantiate the feature’s providers

providerCollection = new SampleFeatureProviderCollection(); ProvidersHelper.InstantiateProviders(

sc.Providers,

providerCollection,

typeof(SampleFeatureProvider));

providerCollection.SetReadOnly();

defaultProvider = providerCollection[sc.DefaultProvider]; if (defaultProvider == null)

{

throw new ConfigurationErrorsException(

“The default feature provider was not specified.”, sc.ElementInformation.Properties[“defaultProvider”].Source, sc.ElementInformation.Properties[“defaultProvider”].LineNumber);

}

}

catch (Exception ex)

{

initializationException = ex; isInitialized = true;

throw ex;

}

isInitialized = true; //error-free initialization

}//end of lock block }//end of Initialize method

//Public feature API //snip...

}

}

The method first attempts to quickly return whether the feature was already initialized; if the initialization caused an error the exception that caused the failure is thrown instead. Because this sample feature depends on a static constructor though, this type of check is not actually needed. I show it here so that you can see how the ASP.NET provider-based features carry out their initialization logic. In the case of

355

Chapter 9

the ASP.NET 2.0 static feature classes, the first if block is what runs 99.9% of the time this type of method is called, so the overhead of calling into Initialize from the public API is normally just the overhead of an extra method call.

However, if the Initialize method detects that the feature has not been initialized, the method enters a synchronization block using the C# lock syntax. Immediately after entering the lock section (now a maximum of one and only one thread can ever be running inside of the lock block), the method double-checks the initialization results. This is the classic lock-and-double-check approach to performing common synchronization for a class. Because, theoretically, two threads of execution may have simultaneously entered the static method, the code makes a second check against the initialization flag to cover the case where a second thread completed initialization after the first thread checked the Boolean isInitialized variable.

Again this static feature class is written a little bit differently from how ASP.NET provider-based features are written. For historical reasons, the ASP.NET provider-based features didn’t use static classes until later in the development cycle. As a result, their initialization processes depended on having a call to a private initialization method inside of every public method and property. This would be equivalent to having the sample class above calling Initialize from inside of the Provider and Providers properties as well as the GetMeAString method. Because the ASP.NET approach didn’t use a static constructor, the feature class needed to provide its own synchronization (like that shown previously) during initialization because it was very likely that there would be multiple threads running inside of the initialization method. The sample feature class though calls Initialize from its static constructor, so it isn’t really necessary to use the first if-check or the lock block with the second if-check. Instead

the Framework will ensure thread safety when the Initialize method is called from the static constructor — and because the method is called from the static constructor it never needs to be called again from the public properties or methods on the sample feature class.

The try-catch block is where the meat of the feature initialization occurs. Using the

ConfigurationManager class in System.Configuration, the Initialize method gets a strongly typed reference to the configuration section class for the feature (this class is later in this chapter). The feature’s configuration section class exposes two important properties: DefaultProvider and Providers. These properties define the default provider that the static feature class should use as well as the set of all configured providers for the feature. If the configuration section in the application’s configuration file is wrong, and it lacks definitions of a default provider and at least one feature provider, the initialization process throws a ProviderException indicating the problem.

With the configuration information now available, the Initialize method creates an empty SampleFeatureProviderCollection class that will eventually hold a reference to each provider that was configured for the feature. This collection is also accessible from the static feature class’s Providers property. The ProvidersHelper class is called to populate this provider collection based on the providers defined in the application’s configuration file. Assuming that the helper successfully ran to completion, the provider collection is then marked as read-only. You don’t want application developers to be able to modify the feature’s provider collection after initialization has occurred.

The Initialize method then attempts to get a reference to the default provider and make it available from the static feature class’s DefaultProvider property. If there is no provider in the provider collection with a Name that matches the value of the feature’s “defaultProvider” configuration attribute, then a ConfigurationErrorsException is thrown. Assuming that the application is running in a high enough trust level, the error message that is returned from the exception will include the file path to the configuration file as well as the line number on which the problematic “defaultProvider” attribute for the feature was defined.

356

The Provider Model

By this point, the Initialize method is able to complete without error, or it catches whatever exception occurred. For either case the feature marks itself as being initialized. In the error case, it also stores a reference to the exception that caused initialization to fail. This is another point where the ASP.NET providerbased features are a little different than the sample feature. The ASP.NET provider-based features need to store the exception and rethrow it whenever their private initialization methods are called from their public properties and methods. However, the sample feature class shown previously instead relies on the Framework to do the heavy lifting.

Because Initialize was called from the static constructor, the Framework will remember that the static constructor failed. This means if the Initialize method fails, subsequent attempts to call public properties or methods on the static feature class result in a System.TypeInitializationException being thrown. The InnerException property on this exception instance will represent the true exception that was thrown from inside of the Initialize method. From a programming standpoint either the ASP.NET approach or the approach shown previously that relies on a static constructor is valid. The decision is up to you.

Using the static constructor eliminates the need for funky lock logic, but you do need to drill into the TypeInitializationException to find the root cause of a failure. The ASP.NET approach means that you will always have the problematic exception being thrown from public APIs and properties. But you will need to use locking inside of your feature’s initialization logic and have each public property and method on your feature class call back to your initialization method to cause the initialization exception to be rethrown.

At this point, let’s take a look at the feature’s configuration section class. You want a configuration class that provides strongly typed access for a configuration that looks like:

<sampleFeature defaultProvider=”DefaultSampleFeatureProvider”> <providers>

<add name=”DefaultSampleFeatureProvider” type=”SampleFeature.SampleFeatureProviderImplementation, SampleFeature” connectionStringName=”SomeConnectionString”

color=”red”

food=”burgers”

description=”this came from config” /> </providers>

</sampleFeature>

The feature itself has its own configuration section as indicated by the <sampleFeature /> configuration element. The one allowable attribute on this element is the “defaultProvider” attribute. Nested within a <sampleFeature /> is a <providers /> section allowing for one or more provider definitions. Aside from the “name” and “type” attributes, all of the other attributes are feature-specific.

The configuration section class that models this configuration section is shown here:

using System;

using System.Configuration;

namespace SampleFeature

{

public class SampleFeatureConfigurationSection : ConfigurationSection

{

357

Chapter 9

public SampleFeatureConfigurationSection(){}

[ConfigurationProperty(“providers”)] public ProviderSettingsCollection Providers

{

get

{

return (ProviderSettingsCollection)base[“providers”];

}

}

[ConfigurationProperty(“defaultProvider”, DefaultValue = “DefaultSampleFeatureProvider”)]

[StringValidator(MinLength = 1)] public string DefaultProvider {

get

{

return (string)base[“defaultProvider”];

}

set

{

base[“defaultProvider”] = value;

}

}

}

}

Inheriting from ConfigurationSection means that this class represents a configuration section in an application configuration file. The default constructor is used by the configuration system when it new()’s up configuration section classes while parsing configuration. The only custom code that you need to write in the configuration class are the custom properties that represent configuration attributes and nested configuration sections.

The Providers property represents the nested <providers /> configuration section. The declarative attribute on the property causes the configuration system to parse the <providers /> section and its nested elements into an instance of a ProviderSettingsCollection. By using the ProviderSettingsCollection class, you automatically leverage the built-in behavior of the <providers /> configuration section without the need to write any additional code.

The DefaultProvider property has two declarative attributes on it. The ConfigurationProperty attribute indicates that if a “defaultProvider” attribute is found within the <sampleFeature /> element that its value will be available via the DefaultProvider property. The ConfigurationProperty also has a default value indicating that the property should be set to “DefaultSampleFeatureProvider” if the attribute is not found in the configuration file. Last, the StringValidator attribute tells the configuration system that if the attribute exists in configuration, the attribute must be a non-empty string. This type of declarative validation rule is automatically enforced when the configuration system attempts to parse the configuration.

In the SampleFeatureMainEntryPoint.Initialize method, the following code is what triggers the parsing and loading of the configuration section:

358

The Provider Model

SampleFeatureConfigurationSection sc = (SampleFeatureConfigurationSection)ConfigurationManager.GetSection(

“sampleFeature”);

The configuration runtime knows to associate the <sampleFeature /> configuration section with the SampleFeatureConfigurationSection class once you add the following section definition to your application’s configuration file:

<configSections>

<section name=”sampleFeature” type=”SampleFeature.SampleFeatureConfigurationSection, SampleFeature” allowDefinition=”MachineToApplication” />

</configSections>

A <section /> element is used to associate an XML element called sampleFeature to the custom configuration class you just saw. The type attribute tells the configuration system where to find the class; in this case the class is located in an unsigned assembly called SampleFeature.dll. Depending on whether you are defining the custom section for a web application, you can also use the “allowDefinition” attribute to control the inheritance behavior of the configuration section. Because provider-based features usually don’t allow redefinition in the level of individual subdirectories, the “allowDefinition” attribute is set to limit the “sampleFeature” element to only machine.config, the root web.config or an application’s web.config file.

At this point, the only piece left to implement for the sample feature is a concrete provider. The basic implementation of a concrete provider (less the initialization step) is:

using System;

using System.Configuration;

using System.Configuration.Provider;

namespace SampleFeature

{

public class SampleFeatureProviderImplementation : SampleFeatureProvider

{

private string color; private string food;

private String connectionString;

public override string Color

{

get { return color; }

}

public override string Food

{

get { return food; }

}

public override string GetMeAString(string andPutThisOnTheEndOfIt)

{

return “This string came from the “ +

“ SampleFeatureProviderImplementation.\r\n” +

“The provider description is: “ + Description + “\r\n” + “The provider color is: “ + Color + “\r\n” +

359

Chapter 9

“The provider food is: “ + Food + “\r\n” + andPutThisOnTheEndOfIt;

}

//Initialize method snipped out for now...

}

}

The concrete provider implementation inherits from SampleFeatureProvider and overrides the two abstract properties as well as the single abstract method defined on the provider base class. The value of the public properties is established when the provider is initialized, while the public method simply plays back the property values as well as some extra strings. Assuming that you configure an instance of SampleFeatureProviderImplementation as the default provider in configuration, a call to SampleFeatureMainEntryPoint.GetMeAString is simply forwarded to the method implementation shown previously. Remember that the forwarding code in the static feature class references the static Provider property, which contains a reference to the default provider defined in configuration:

public static string GetMeAString(string someString) {

return Provider.GetMeAString(someString); }

This is the same approach used by most of the ASP.NET 2.0 provider-based features and explains why you can use static classes like Membership and these classes just work because their static methods internally forward their calls to the default feature provider.

Of course, the concrete provider really can’t accomplish anything unless it is initialized first:

public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)

{

if ( (config == null) || (config.Count == 0) ) throw new ArgumentNullException(

“You must supply a non-null, non-empty value for config.”);

if (string.IsNullOrEmpty(config[“description”]))

{

config.Remove(“description”);

config.Add(“description”,

“This would be where you put a localized description for the provider.”);

}

//Let ProviderBase perform the basic initialization base.Initialize(name, config);

//Perform feature-specific provider initialization here

//Color

if (string.IsNullOrEmpty(config[“color”]))

{

color = “The default color for the provider”;

}

else

{

color = config[“color”];

360

The Provider Model

}

config.Remove(“color”);

//Food

if (string.IsNullOrEmpty(config[“food”]))

{

food = “The default food for the provider”;

}

else

{

food = config[“food”];

}

config.Remove(“food”);

//Get the connection string

string connectionStringName = config[“connectionStringName”]; if (String.IsNullOrEmpty(connectionStringName))

throw new ProviderException(

“You must specify a connectionStringName attribute for the provider”);

ConnectionStringsSection cs = (ConnectionStringsSection)ConfigurationManager.GetSection(

“connectionStrings”);

if (cs == null)

throw new ProviderException(

“The <connectionStrings/> configuration section was not defined.”);

if (cs.ConnectionStrings[connectionStringName] == null) throw new ProviderException(

“The connectionStringName could not be found “ +

“in the <connectionStrings /> configuration section.”);

else

connectionString = cs.ConnectionStrings[connectionStringName].ConnectionString;

if (String.IsNullOrEmpty(connectionString)) throw new ProviderException(

“The specified connection string has an invalid value.”); config.Remove(“connectionStringName”);

//Check to see if unexpected attributes were set in configuration if (config.Count > 0)

{

string extraAttribute = config.GetKey(0); if (!String.IsNullOrEmpty(extraAttribute))

throw new ProviderException(“The following unrecognized attribute was “ + “found in the “ + Name + “‘s configuration: ‘“ + extraAttribute + “‘“);

else

throw new ProviderException(“An unrecognized attribute was “ +

“found in the provider’s configuration.”);

}

}

361