Home   |   QuickStart Welcome   |   ASP.NET   |   Web Services   |   How Do I...?   
  |   I want my samples in...      

ASP.NET 2.0 Quickstart Tutorial

Extending ASP.NET

What's New in 2.0

ASP.NET 2.0 was designed with pluggability and extensibility in mind. You can replace many of the built-in modules and behaviors in ASP.NET with your own custom implementations. Among the improvements to extensibility in this release are:
  • The Page framework lifecycle - has additional steps and processes;
  • Server controls runtime - Custom controls can be written to take advantage of new features such as:
    1. State management such as view-state and control-state, or requesting encryption of the page-state;
    2. You can write control and page adapters where you require your control or page to handle behaviors that are dependant on the browser, device or device class or the markup. Adapters plug into the runtime lifecycle;
    3. The new data-source and data-bound controls mean you can add declarative data binding support for new data storage providers, by writing custom data-source controls that represent those storage providers;
    4. Web resources, to embed a resource like client-side script files;
    5. Use the enhanced client script management functionality;
    6. Make use of the theming or device filtering features in ASP.NET, or;
    7. Call-backs - Where a page developer or control developer wishes to perform actions that require communication with the server, without an entire post-back, they can make use of call=backs;
  • Page-state - In addition to the handling of view-state and control-state in the control you can also extend the page-state persister used to handle the collective page state;
  • Server controls design-time - Designer classes associated to server controls provide the means to edit the control in a visual designer. You can write control designers to provide rich editing features like region-editing, task-based editing, painting and template-editing;
  • Application service providers - All of the built-in services in ASP.NET 2.0 are completely extensible through a provider-based registration model. This includes Membership, Roles, Personalization, Site Counters, Profile, Session State, Site Navigation, and Web Events.
  • Expression builders - You can add custom expression builders to add declarative support for substituting values into a page prior to the parsing and compilation phases. Expression builders can provide both runtime and design-time support;
  • Localization - Rather than using ResX files to support localization, you can implement your own resource providers to support the runtime and design-time localization model that obtains data from another source such as a backend data store;
  • Compilation and pre-compilation - You can add new file format extensions into the ASP.NET compilation system using build providers. For example, ASP.NET 2.0 uses build providers to process WSDL files for compiling Web service proxies and XSD files for compiling strongly-typed data sets in the App_Code directory. If you want to 'virtualize' your web content from a source other than traditional files, then you can implement a virtual path provider.
This section describes these and other general extensibility features in ASP.NET 2.0.

ASP.NET 2.0 was designed with extensibility in mind. While ASP.NET 2.0 strives to meet the demands of most applications with out-of-the-box features, we understand that it is very important to be able to extend those features with your own implementations when your application calls for a behavior outside the scope of the built-in features. Fortunately, nearly every part of the ASP.NET 2.0 system is replaceable or extendable.

Page Framework Lifecycle

The page framework lifecycle is a process that begins with the page and recursively drills into the controls and child controls within that page. At each point in the lifecycle there are specific operations that constitute the behavior of the page and the control and understanding this is important for control extensibility, page extensibility and specific page development features. For example:
  • Page.InitializeCulture where you can explicitly handle the UICulture and Culture for localization, e.g. from Profile;
  • Page.OnPreInit, Page_PreInit, where you can dynamically set the Page.Theme or Page.MasterPageFile;
  • Control.LoadControlState, Control.SaveControlState to explicitly handle new state management functionality;
  • Other new page events, like OnInitComplete, OnPreLoad, OnLoadComplete, OnPreRenderComplete, OnSaveStateComplete etc.

Server Controls Runtime

User Controls, (.ASCX) are 'compositional controls' that are written declaratively using the same techniques used to build a declarative page. Visual Studio now provides a 'preview' of user-controls in the design-view when these controls are displayed in the consuming page. Custom controls, (code files, assemblies) are programmatically created and provide extensibility in many forms, (composition, inheritance etc.). When writing a custom control, it is important to understand a control's lifecycle stages, including the handling of state information and any post-back handling.

When creating custom controls you can compile your controls into assemblies that are added to the application's Bin directory or in ASP.NET 2.0, you can add code file(s) to the application's App_Code directory. In this case ASP.NET 2.0 will dynamically compile the control. You can also in ASP.NET 2.0 reuse tag prefixes when registering controls from multiple namespaces. So it is possible to create several controls in several namespaces that all contribute to the same tag prefix.

<%-- Register the controls defined in an assembly that resides in the Bin directory --%>
<%@ Register TagPrefix="Demos" NameSpace="DemoControls" Assembly="MyDemoControls" %>

<%-- Register the controls defined in code that resides in the App_Code directory --%>
<%@ Register TagPrefix="Demos" NameSpace="DemoControls" %>

<Demos:ControlFromBinAssembly runat="server" ID="CustomControl1" />
<Demos:ControlFromCodeFile runat="server" ID="CustomControl2" />

Control Classes

ASP.NET introduced the Control and WebControl base classes, and ASP.NET 2.0 adds more features to these classes along with new classes for example the CompositeControl. This simple base class overrides the Controls collection and provides a mechanism for an associated CompositeControlDesigner to ensure the child controls are always created.

public class MyCompositionControl : CompositeControl {
	protected override void CreateChildControls() {
		// Create control collection ..

The CompositeControl is a WebControl and implements INamingContainer. The Controls collection calls EnsureChildControls. You would create all child controls in CreateChildControls. The base class provides design-time support, again ensuring the Controls collection is created.

You can enable a page developer to style your control by exposing top-level properties that delegate to the child control style properties. If you have many properties and controls, then consider exposing properties of type Style and applying these during render methods so that child controls do not persist these as view-state, but rather the parent handles state management of these complex properties.

Control Class and Property Meta-data

There are a number of interesting class-level and property-level attributes used for features in ASP.NET 2.0 some of which are:
  • LocalizableAttribute: Localization is enabled for all controls and objects, including static markup in a web application through the use of expressions, (both implicit and explicit). These features are 'layered' on a control at parse-time and you do not need to add any special handling to your control. During design-time, Visual Studio will 'push' values to a neutral resource for properties that are marked with the LocalizableAttribute.
  • ThemeableAttribute: Page developers and developers performing customization can take advantage of themes to affect the stylistic appearance and to some extent the content of a page or site. All properties are by default theme-able, so if there are properties that you decide should not be theme-able, such as sensitive data properties or properties that are not style-based, mark them as Themeable(false).
  • UrlPropertyAttribute: Where a control could be used in master-page for example, or other such types then Url type properties will potentially require re-basing (an image or a style sheet). In order to base the Url property value to the consuming page, e.g. a content-page in a master-page, content-page relationship the page framework will rebase that Url when marked with this attribute;
  • WebResourceAttribute: Used to mark the assembly that contains an embedded resource, so that resource can be served in a request;
  • FilterableAttribute: Used to mark control properties, or controls that wish to support the device filtering, declarative syntax in the page.
Note: that when creating custom controls, Visual Studio intellisense and validation will interpret types and class and property meta-data attributes to create the schema.

State Management

In ASP.NET 1.x, it was common for controls to store data in the view-state dictionary in order to round-trip data across post-backs. Page developers could disable view-state however, and consequently lose some of the core control behavior as that data was also round-tripped in view-state. In ASP.NET 2.0 you can take advantage of control-state if you have a requirement to keep some state regardless of the setting the page developer makes. Items stored in control-state should be limited: a current page index or a data key value. Be prudent using this form of state, it is not intended to be a repository for large amounts of data. Custom controls need to register for control-state (ideally OnInit but before OnLoad) and provide explicit save and load handling. You'll need to register on every post-back.

protected override void OnInit(EventArgs e) {
protected override object SaveControlState() {
	object o = base.SaveControlState();
	return new Pair(o, ViewIndex);
protected override void LoadControlState(object state) {
	Pair p = state As Pair;
	if (p != null) {
		ViewIndex = (int)p.Second;

In ASP.NET 2.0, the control-state and view-state are combined in the hidden field in the page rendering. You can make a request to the page to encrypt the field, (Page.RegisterRequiresViewStateEncryption) if you need to round-trip potentially sensitive data.

Control and Page Adapters: Handling Behavior for Devices, Browsers or Markups

The 'behavior' of a control, (e.g. the rendering) is normally handled within the class for the control (using the associated text writer and HttpBrowserCapabilitites). You can, however, create configurable page and control adapters that handle the behavior on behalf of the control. Such configuration is made within the browser capabilities files, (.browser).

The HttpBrowserCapabilities creation process uses declarative browser files that reside in the install directory under Browsers and/or the application's local App_Browsers directory. The following snippet shows a custom browser file that maps a custom adapter for a custom control; the assembly containing the adapter is created in the App_Code directory of the application.

<!-- New browser node links into parent -->
<browser id="My_PIE_PPC" parentID="PIEnoDeviceID">
        	<capability name="browser" match="Pocket IE" />
        	<capability name="majorversion" match="4" />

	<!-- Define new text-writer for this browser and define control-adapter mapping -->
    	<controlAdapters markupTextWriterType="MyNamespace.MyCustomTextWriter" >
	<adapter controlType="MyNamespace.MyControl" adapterType="MyNamespace.MyControlAdapter" />

During the page framework lifecycle, ASP.NET 2.0 attempts to look for an adapter on the control using the Control.ResolveAdapter method. If an adapter is returned, the page framework will call on the adapter methods instead of the control's lifecycle methods. As an extensibility developer you can extend the adapter base classes, e.g. ControlAdapter, WebControlAdapter and PageAdapter and define overrides for the lifecycle stages and other methods such as child control creation and post-back handling.

  • The base adapter classes call back on the control's equivalent method. It is typical therefore, to call on the base method in your adapter. This is not necessarily true of the Render method(s), as you could end-up with unwarranted markup behavior;
  • Adapters can implement IPostBackEventHandler and IPostBackDataHandler to handle post-back data and raise events;
  • Adapters can implement view-state and control-state methods, LoadAdapterViewState, SaveAdapterViewState and LoadAdapterControlState, SaveAdapterControlState. These methods contribute to the control's behavior however;
  • Adapters can implement CreateChildControls and handle data-binding calls;
  • Alternatively you can prevent 'adaptation' for your custom control by returning null from Control.ResolveAdapter.

// Control
public class MyControl : Control {
	// Example showing override of Render to control entire rendering
	protected override void Render(HtmlTextWriter writer) {
		Style s = new Style();
		s.ForeColor = System.Drawing.Color.Green;
		writer.Write("Welcome custom control.");
// Adapter	
public class MyControlAdapter : ControlAdapter {
       	// Example showing override of Render to control entire rendering
	protected override void Render(HtmlTextWriter writer) {
		Style s = new Style();
		s.ForeColor = System.Drawing.Color.Red;
       		writer.Write("Welcome custom control adapter.");            

Data Source and Data Bound Controls

A data source control represents a backend data storage provider that can expose data to data-bound UI controls. ASP.NET ships with several data source control, for example data sources representing SQL databases, middle-tier objects, or XML files. All data source controls have the following in common:
  • Represent one or more named views of data
  • Each view provides an enumeration of objects
    • Essentially SELECT in SQL terms
  • A view may also be able to perform editing operations on its collection of objects
    • Essentially UPDATE, INSERT, DELETE in SQL terms.
    • Capabilities model exposed as boolean properties
  • Ability to trigger change events
  • Ability to load data on-demand
You can easily add support for additional data sources by creating a custom data source control, which is a server controls that implements either the IDataSource or IHierarchicalDataSource interface, or both. ASP.NET 2.0 also includes the DataSourceControl and HierarchicalDataSourceControl abstract base classes that encapsulate the common behavior that most data source controls need.
public interface IDataSource {
    event EventHandler DataSourceChanged { add; remove; }
    DataSourceView GetView(string viewName);
    ICollection GetViewNames();

public abstract class DataSourceControl : 
  Control, IDataSource, IListSource {
A DataSourceView represents the various operations that can be performed over a view of the data that the data source control exposes. The following is a simplification of the actual DataSourceView base class (in actuality, the methods follow an asynchronous design pattern).
public abstract class DataSourceView {

    public virtual bool CanDelete { get; }
    // CanInsert, CanPage, CanSort, CanUpdate, ...

    public event EventHandler DataSourceViewChanged;
    public virtual int Delete (IDictionary keys, IDictionary oldValues, ...);
    public virtual bool Insert (IDictionary values, ...);
    public virtual int Update (IDictionary keys, IDictionary values, IDictionary oldValues, ...);
    public abstract IEnumerable Select(DataSourceSelectArguments arguments);
The .NET Framework reference documentation for these classes includes examples for how to write a custom tabular or hierarchical data source control.

Web Resources

In ASP.NET 1.x developers writing custom controls that required custom resources such as images or client script would need to install the resources in the aspnet_client virtual folder. In ASP.NET 2.0 you can take advantage of web resources to simplify the process. Web resources allow resources to be embedded in an assembly and are retrieved through the web resources handler.

// Mark the assembly with the resource
[assembly: WebResource("MyClientScript.js", "text/javascript")]

public class MyControl : WebControl {
	protected override void OnInit(EventArgs e) {
		// Register the script to be rendered as a link
                String sScript = Page.ClientScript.GetWebResourceUrl(typeof(MyControl), 
                Page.ClientScript.RegisterClientScriptInclude("MyInclude", sScript);
	protected override void OnPreRender(EventArgs e) {
		this.Attributes["onmouseover"] = "MouseOverScript()";

Client Script Management

The Control.Page property exposes a ClientScript property that encapsulates features for handling, registering and referencing client-script. When combined with web resources, it is possible for you to also embed those scripts within the assembly of the control, see the sample in web resources.

public class MyButton : Button {
	protected override void OnPreRender(EventArgs e) {
		String sScript = "function DoAlert(){alert('Hello World');}";
			"ScriptFunction", sScript, true);

                OnClientClick = "javascript:DoAlert();";

Device, (device markup or browser) Filtering

A page developer can use a new declarative syntax to qualify control properties so that they are only set when a device filter definition evaluates to true. For example, a device filter ID may define the Internet Explorer class of browsers, as defined in browser files in the application. The device-filter can be used to declaratively qualify properties of a control. You do not have to add anything to your custom control to support this. However, in cases where you do not wish device filters to apply simply mark your control or property with the Filterable(false) attribute. Note that localization also takes device filtering into account too.
<!-- Text property is defaulted to a value, reset given IE and also if the custom filter applies. --> 
<!-- The more specific the device-filter wins.. -->
<asp:Label runat="server" Id="WelcomeLabel" Text="Welcome to ASP.NET's Quickstarts" 
	IE:Text="Quickstarts" MyFilter:Text="Welcome" />


Call-backs allow a control or page to perform post-back to the server in a manner that does not require the page to be posted back entirely. You can easily combine for example, web resources with client script management to also make use of the call back infrastructure. To enable a call back you will
  • Create a call back event with a given signature in your class. (ICallbackEventHandler);
  • Create client-side script that will manage the return call or error;
  • Use the call back event reference hooked up to a client-side event in your custom control.

public class MyControl : CompositeControl, ICallbackEventHandler {
        public string ICallbackEventHandler.RaiseCallbackEvent(string eventArgument) {
            if (eventArgument == "Dogs") 
                return "Labrador";
                throw new ApplicationException("Only Dogs allowed!");
	// Client script functions that handle the callback
	private String sButtonCallBack = 
		"function ButtonCallBack(result, context){ alert(result);}";
	private String sButtonCallBackError = 
		"function ButtonErrorCallBack(result, context){ alert(result);}";

        protected override void OnInit(EventArgs e) {
			"ButtonCallBack", sButtonCallBack, true);
			"ButtonCallBackError", sButtonCallBackError, true);
	protected override void OnPreRender(EventArgs e) {
            // Set up the OnClick event to fire an out-of band call to the handler
		Attributes["OnClick"] = Page.ClientScript.GetCallbackEventReference(this, 
			"'Dogs'", "ButtonCallback", "null", "ButtonErrorCallback", true);


Page-state, (the cumulative view-state and control-state for the controls and the page) is typically persisted to the page's response, using the HiddenFieldStatePersister (it is the VIEWSTATE hidden field). You can override the Persister on the Page for custom persistence however. Given the adaptive nature of controls to the requesting device, some devices may not handle significant amounts of data that are typically 'round-tripped'. Through the PageAdapter configured for a device, the state can be persisted to alternative sources. ASP.NET 2.0 defines two state persisters, HiddenFieldPageStatePersister and SessionPageStatePersister.

ASP.NET 2.0 also enables the splitting up the single hidden field into several using the maxViewStateFieldLength attribute in configuration.

Server Controls Design-time

There are many improvements, simplifications and advances in control designers. You write a ControlDesigner to provide design-time behavior for your custom control in a development environment like Visual Studio. The following features are available in ASP.NET 2.0:
  • Region-based editing. In ASP.NET 1.x you would use a ReadWriteControlDesigner to provide a single read/write area for your control on the design-surface. In ASP.NET 2.0 you can create editable, template-able and read-only regions (DesignerRegion types) for richer editing experiences. Regions can also provide tool-tips and highlighting. You can also handle click events on the control and interact with the regions;
  • Painting on the design-surface. You can provide graphics on the design-surface for your control by indicating that the control supports painting and overriding OnPaint;
  • Control-designer types can interact with the control more closely with accessors but also can take advantage of control features such as embedded resources like images;
  • Task-based editing. Using DesignerActionLists you can provide a very visual, contextual task list for page developers to interact with. This advances the previous concept of DesignerVerbs;
  • Improved and much simplified template-editing. Your control-designer can defer simply to the tool's template editing UI or you can create your own UI using regions. Adding template editing is now as simple as defining the TemplateGroups collection on the ControlDesigner;
  • Several new convenient control-designer base classes, CompositeControlDesigner and ContainerControlDesigner for common scenarios.
The following snippet shows a very simple WebControl and associates a ContainerControlDesigner. In this form, the designer tool creates a single, editable region for the control, which allows drag-and-drop. The control defines children as child controls, see ParseChildren(false) and PersistChildren(true). The ControlDesigner creates a caption bar and sets styles accordingly. The pseudo code also shows steps to implement a simple DesignerActionList.

[Designer(typeof(MyDesigner)), ParseChildren(false), PersistChildren(true)]
public class MyControl : WebControl { }

public class MyDesigner : ContainerControlDesigner {
	private Style _style = null;
	public override string FrameCaption { get { /* Return a caption */"; } }
	public override Style FrameStyle { get { /* Return a Style for the frame*/ } }

	public override DesignerActionListCollection ActionLists {
		get { 
			// Get the base collection and add my own list
			al.Add(new MyList(this));	
			return al;
	private sealed class MyList : DesignerActionList {
		// Members to handle the smart-tag
		public override DesignerActionItemCollection GetSortedActionItems() {
			// Create a collection and add DesignerActionTextItem, 
			// DesignerActionPropertyItem and DesignerActionMethodItem types

Application Service Providers

Many ASP.NET application services have been designed with a provider-based model. Providers abstract the physical data storage for a feature from the classes and business logic exposed by a feature. Features that are built on top of providers allow you to create your own custom providers and configure them to work with the feature. Custom providers allow developers to implement specialized business logic and to operate against alternate data stores. Pages that use a provider-based feature continue to work normally with custom providers, and without any changes in page code. The following application services support the provider model and allow you to author and configure custom providers:
  • Membership
  • Role Manager
  • Session State
  • Profile
  • Site Navigation
  • Site Counters
  • Web Parts Personalization
  • Web Events

Expression Builders

ExpressionBuilders in ASP.NET 2.0 are a parse-time feature that allows the page developer to add declarative syntax to assign to a control's properties. ASP.NET 2.0 ships with expression builders for:
  • Connection strings. Used to access connection-strings in configuration. <%$ connectionstrings: MyConnStr %>;
  • Application settings. Used to access the application setting in configuration. <%$ appsettings: MyAppValue %>, and;
  • Resources. Used as the foundation for building a multi-lingual site. <%$ resources: mykey %>.
Expression builders can offer design-time handling through the Expressions dialog in Visual Studio which is accessed through the property grid. You can add your own expression builders and expression prefix and not only handle the expression during runtime but you can also support the design-time too.

Expression builders can also be constructed to support the no compile feature of ASP.NET 2.0. Instead of ASP.NET generating code through the expression builder during the generation of the page-class, the runtime instead instantiates the expression builder and evaluates the expression. The following example shows a pseudo ExpressionBuilder and ExpressionBuilderEditor. .

// In configuration, add your expression builder. Here the type is defined in App_code
	<add expressionPrefix="MyExpr" type="MyExpressionBuilder" />

// In code, create Expression builder and define the prefix, the editor etc.
[ExpressionPrefix("MyExpr"), ExpressionEditor("MyExpressionEditorDesigner"]
public sealed class MyExpressionBuilder : ExpressionBuilder {
        public override bool SupportsEvaluate {
            get {
                return true; // Supports the evaluation for no compile scenarios

        public override CodeExpression GetCodeExpression(..) {
		// Return a code expression, given the expression

        public override object EvaluateExpression(..) {
            // Return an evaluation of the expression for no compile scenarios

// The editor is used at design-time.
public sealed class MyExpressionEditor : ExpressionEditor {
       	public override ExpressionEditorSheet GetExpressionEditorSheet(..) {
		// Create a sheet
       		return new MyExpressionEditorSheet(..);

        public override object EvaluateExpression(..) {
		// Return an evaluation during design-time

       	private sealed class MyExpressionEditorSheet : ExpressionEditorSheet {

		// Expose any properties that represent the expression's value, you can define
		// default values, type converters and descriptions


The localization model in ASP.NET 2.0 allows the page developer to set implicit or explicit, declarative expressions. You can extend the resource expressions as described above and still use the underlying resource providers. More likely however, is extending localization to provide an alternative source for resource data rather than use ResX and it's resultant satellite assemblies. You can simply swap out the existing ResourceProviderFactory that is defined in configuration and supply a new one. The factory creates IResourceProviders for both global and local resources. The following pseudo code demonstrates a simple case (the custom factory must be defined in configuration).

// In configuration. Here the type is defined in App_Code
<globalization resourceProviderFactoryType="MyResourceProviderFactory" ../>

// In code. Note that to support design-time too, add the following attribute and
// define the provider
public sealed class MyResourceProviderFactory: ResourceProviderFactory {
       	public override IResourceProvider CreateGlobalResourceProvider(string classKey) {
       		return new MyGlobalResourceProvider(classKey);
       	public override IResourceProvider CreateLocalResourceProvider(string virtualPath) {
       		return new MyLocalResourceProvider(virtualPath);

	private sealed class MyGlobalResourceProvider: IResourceProvider {
       		object IResourceProvider.GetObject(string resourceKey, CultureInfo culture) {
			// Get a resource object for the resource key and culture
		IResourceReader ResourceReader {
       			get { /* return a reader that enumerates the neutral resource */ }

Compilation and Pre-compilation

ASP.NET 2.0 defines several ways to compile a web application and several extensibility points.

Dynamic compilation includes for example, code separation files associated with web pages and user controls, code in the App_Code directory and abstract files such as ASPX, ASCX, RESX, and also, WSDL, XSD.

There are two forms of pre-compilation of sites for deployment as well as a pre-compilation mode that 'primes' the site. Pre-compiling for deployment can be performed in two ways that either allows specific updates to the compiled site or not. In both cases code is removed from the compiled site (target). Where no updates are selected, the declarative files like ASPX are removed too. Pre-compiling to prime a site means that ASP.NET will perform regular compilation of the entire site to avoid the first-time request penalty.

To precompile a site, you can use the aspnet_compiler.exe tool. This tool is located in the .NET Framework installation directory, for example %WINDIR%\Microsoft.NET\Framework\<version>. You can run the tool with a /? switch to see the list of available options. For example, to pre-compile a site in-place by specifying the virtual path to the application, use the following command line:

  > aspnet_compiler.exe -v /MyApp c:\MyTarget

You can also extend the compilation processes in a number of ways; by contributing new file types or extensions to the compilation using BuildProviders, or by making specific content virtual so that the source is for example not on disk. The VirtualPathProvider provides this extensibility point.

Build Providers

Build providers are associated to a file extension through configuration. The build provider produces code for a virtual path that is compiled by ASP.NET. With a build provider you can add source to the compilation processes of ASP.NET or define new abstract content that you handle the parsing of to supply code to ASP.NET.

// In configuration
<compilation ..>
		<add extension=".myext" type="MyBuildProvider, .." appliesTo="All"/>

public class MyBuildProvider : BuildProvider {
	public override void GenerateCode(AssemblyBuilder assemBuilder) {
		// Return code, e.g. a CodeCompileUnit that is added to the 
		// assembly builder. This will involve custom parsing and code generation	
       	public override System.Type GetGeneratedType(CompilerResults results) {
		// Return the type generated (e.g. MyNamespace.MyClass)

If you need to obtain a type in application code you can use the BuildManager.GetType method as assembly names are not predictable.

Virtual Path Provider

ASP.NET allows you to provide content for web 'content', such as an ASPX through a mechanism called a VirtualPathProvider. The compilation process will call on registered VirtualPathProviders to supply the content. You need to register your virtual path provider at an early point in the application's lifecycle, for example in a special method you can define in code in the App_Code directory, AppInitialize. Only specific types of web content can be virtualized in this manner. For example, code in the App_Code directory cannot.

Your virtual path provider can return content for a virtual path, or simply hand off to the previous provider in the chain.

// In code in the App_Code directory
public sealed class MyVirtualPathProvider : VirtualPathProvider {

	public static void AppInitialize() {
		MyVirtualPathProvider myVpp = new MyVirtualPathProvider();

	// Implement other methods that define file hashing and dependencies ..
	public override bool FileExists(string virtualPath) {
		// Return whether this virtualPath exists, or defer to the Previous.
	public override bool DirectoryExists(string virtualDir) {
		// Return whether this virtualDir exists, or defer to the Previous.
	public override VirtualFile GetFile(string virtualPath) {
		// Handle the virtualPath and return a 
		// VirtualFile or defer to Previous. 
		// VirtualFile implements an Open method to return a Stream
	public override VirtualDirectory GetDirectory(string virtualDir) {
		// Handle the virtual directory and return a 
		// VirtualDirectory or defer to Previous.
		// VirtualDirectory implements enumerations of sub-dirs and files