Plug-Ins

Let Users Add Functionality to Your .NET Applications with Macros and Plug-Ins

Jason Clark

Code download available at:Plug-Ins.exe(135 KB)

This article assumes you're familiar with .NET and C#

Level of Difficulty123

SUMMARY

Most user applications benefit from the ability to be extended by other developers. It's often easier and more efficient to extend an existing application that users are already familiar with and trained on than it is to develop one from scratch. Thus, extensibility makes your application more attractive. You can build extensibility into your application by supporting features like plug-ins or macros. This is easily accomplished using the .NET Framework even if the core application isn't a .NET Framework app. In this article, the author describes extensibility features of the .NET Framework including late binding and reflection and how to use them, along with plug-in security considerations.

Contents

The Ideal Extensible Application
.NET Framework Extensibility Features
Getting Started with Extensibility
Loading: Binding to Code at Run Time
Discovery: Finding Code at Run Time
Activation: Create Instances and Call Methods
Secure Extensibility
Adjusting Security Policy
Extensible Application Design
Versioning
Robustness
Security Considerations
Unloadable Plug-in Assemblies
Managed Plug-ins in Unmanaged Apps
Wrapping It Up

Imagine the perfect text editor. It launches in under two seconds, supports context coloring and automatic indenting for popular programming languages, supports Multiple Document Interface (MDI) as well as that cool tabbed document arrangement that has become so popular. The problem with conceiving the perfect text editor is that perfect is in the eye of the beholder. These features may be my definition of a perfect text editor, but others will certainly have different criteria. Perhaps the most important feature the perfect text editor could have would be rich support for extensibility so that any developer can extend the application with features that they want.

An extensible text editor might support the creation of custom toolbars, menus, macros, and even custom document types. It would allow me to write plug-ins that hook into the editing process so that features like autocomplete, spell-checking, and other niceties could be added. Finally, the perfect text editor would let me write my plug-ins in any of a number of languages (my personal preference is C#).

To be honest, I want every application that I use to be extensible in this way. If I can customize my favorite applications by writing a little code here and there, that is great. And even if I don't, I know that somebody else will and I can then benefit from their extension by downloading it from the Internet. Thus begins my campaign to get all developers to write extensible applications.

The Ideal Extensible Application

Lots of applications can be modified using pluggable code. In fact, the entire Microsoft® Office suite of applications is so extensively customizable that folks write complete custom applications using Office as the platform. And yet, even with all of this customizability, I have yet to write my first plug-in for Microsoft Word (an application that I use nearly every day).

The reason for this is simple. Microsoft Office, with all of its features, simply doesn't meet all of my criteria, which include:

  • Simplicity. I want to be able to manipulate my pluggable application with very simple software tools with which I am already familiar.
  • Access. I want my plug-ins to have access to some subset of the objects and functionality built into the application. This access should be as natural as if it were part of my programming language of choice.
  • Programming language. I want to use my programming language of choice, period.
  • Power. I want a rich API in addition to my access to the Document Object Model (DOM) of the application.
  • Security. I want to be able to download plug-ins that other people have written and made available over the Internet. I want to execute potentially malicious or buggy components without concern for the safety of my system.

This short list is a tall order. In fact, up until the release of the Microsoft .NET Framework, these criteria were simply too strict to be met by the typical application. But now I can show you how to use the .NET Framework to add all of these extensibility features to your managed and unmanaged applications.

.NET Framework Extensibility Features

Extensibility is built on late binding, which is the ability to discover and execute code at run time rather than at compile time (the more typical case). Over the years, many innovations have contributed to late binding, including DLLs and the COM API. The .NET Framework brings the simplicity of late binding to a whole new level. Let's take a look at a very simple code example to get a better understanding.

The code in Figure 1 shows how easy it is to use reflection to perform late binding on a managed object. If you build the code in Figure 1 into LateBinder.exe and run it, you can pass it the name of any assembly (such as one built from the code in Figure 2) as its command-line argument. LateBinder.exe will reflect over the assembly and create instances of the Form-derived classes in the assembly and make them MDI children of itself. Reflection in the .NET Framework makes late binding incredibly simple.

Figure 2 CircleForm.cs

using System; using System.Drawing; using System.Windows.Forms; // Form class that draws a circle in its client area public class Circle:Form{ protected override void OnPaint(PaintEventArgs args){ Int32 diameter = Math.Min(ClientRectangle.Width, ClientRectangle.Height); args.Graphics.FillEllipse(Brushes.Blue, (ClientRectangle.Width-diameter)/2, (ClientRectangle.Height-diameter)/2, diameter, diameter); } }

Figure 1 Late Binding

using System; using System.IO; using System.Reflection; using System.Windows.Forms; using System.Drawing; class App{ public static void Main(String[] args){ // Load the assembly whose name was passed on the command-line Assembly a = Assembly.Load( Path.GetFileNameWithoutExtension(args[0])); // Pass the assembly object to a constructed LateBinderForm Application.Run(new LateBinderForm(a)); } } // Creates late-bound form instances for MDI children class LateBinderForm:Form{ public LateBinderForm(Assembly a){ // Make it MDI IsMdiContainer = true; // Get the types in the assembly Type[] types = a.GetTypes(); // Iterate and find types derived from Form Instantiate them foreach(Type t in types){ if(t.BaseType == typeof(Form)){ Form f = (Form)Activator.CreateInstance(t); f.MdiParent = this; // Set as MDI children f.Show(); } } } }

Reflection is one of the essential tools of the .NET Framework that promotes the development of extensible applications. It is one of four features I'll mention here that you can use to make your applications extensible.

The common type system After using the .NET Framework for a while you may begin to take the Common Type System (CTS) for granted. However, it is really one of the reasons that extensibility is so easy on this platform. The CTS defines a subset of object-oriented features that all managed languages must adhere to, such as rules for derivation, namespaces, object types, interfaces, and primitive types. The CTS sets the ground rules for code run by the common language runtime (CLR).

Reflection Reflection is the ability to discover, at run time, information such as the types implemented by an assembly or the methods defined by a type. Reflection is possible because all managed code is self describing through a data structure embedded in the assembly called metadata.

Fusion The .NET Framework uses Fusion to load assemblies into a managed process (AppDomain). Fusion facilitates such advanced features as strong naming and simplified search rules for DLLs.

Code access security Code access security (CAS) is a feature of the .NET Framework that facilitates the execution of partially trusted code. In a nutshell, you can use features of the Microsoft .NET Framework in order to limit what your late-bound code can access so that you don't have to worry about a plug-in undermining the user's system.

These four features of the .NET Framework make extensibility a reality. However, as cool as these features are, an article on each might not bring the extensibility story to light. Instead, it is best to approach the topic from a task-oriented point of view.

Getting Started with Extensibility

Regardless of what your application does, if it is extensible then it must perform three basic tasks: discovering, loading, and activating extensions. Discovery is the process of finding plug-ins or other code that your application will bind to at run time. Loading is the process of bringing the code (packaged as an assembly) into your process or AppDomain so that types defined by the assembly can be activated and consumed. Activation is the process of creating instances of late-bound objects and calling methods on them.

Surrounding each of these three phases are a number of .NET technologies and application design considerations. Although the technology is in place, the .NET Framework does not define one particular way to implement extensibility; there are quite a lot of options open to you.

Loading: Binding to Code at Run Time

Logically speaking, extensible applications discover code before loading it. But reflection must load code to discover things about it, so in practice the discovery process may require code to be loaded ahead of time. Let's see what I mean.

Reflection can be used to implement late binding. Most of the reflection classes can be found in the System.Reflection namespace. The three most important classes are System.AppDomain, System.Type, and System.Reflection.Assembly. I'll look at System.Type later on. To understand the AppDomain and Assembly classes I'll provide a brief tour of a managed process.

The CLR runs managed code in a Win32® process and the granularity of it is finer than what you would find in unmanaged applications. For example, a single managed process can contain multiple AppDomains which you can think of as a sort of sub-process or lightweight application container.

Assemblies are the managed version of DLL and EXEs and they contain reusable object types such as class library types as well as your application code. Additionally, any extensions or plug-ins to your application would also live in an assembly (most likely a DLL). Assemblies are loaded into one or more AppDomains in your managed process.

Each managed process has at least one default AppDomain, and also contains certain shared resources such as the managed heap, the managed thread pool, and the execution engine itself. In addition to these logical components, a managed process can create any number of additional AppDomains. See Figure 3 for a representation of a managed process containing two AppDomains. AppDomains will become critically important when I cover the topic of plug-in discovery later.

Figure 3 Managed Process

Figure 3** Managed Process **

Now, back to the AppDomain and Assembly types. You can use the static method Assembly.Load to load an assembly into your current AppDomain. The Assembly.Load method returns a reference to the Assembly object. This method binds code to your app at run time by loading the assembly in which the code is housed.

Assembly.Load loads an assembly by name (without extension) from your AppBaseDir directory, the directory from which your AppDomain loads privately deployed assemblies. By default Assembly.Load looks in the directory from which your EXE was loaded for regular executables. Assembly.Load follows the same assembly binding rules that the CLR uses when loading assemblies that are early bound.

You can adjust the AppBaseDir of your AppDomain by obtaining a reference to an AppDomain object and calling the AppendPrivatePath and ClearPrivatePath instance methods on AppDomain. If you use Assembly.Load to load your plug-in assemblies, you are likely to want to manipulate the AppBaseDir of your AppDomain. This is because it is useful to maintain your plug-ins in a subdirectory of your main application directory. I will discuss more reasons for this shortly.

The Assembly class also implements a method called LoadFrom which, unlike Load, takes the full name of your assembly file including the path and extension. LoadFrom simply loads the file that you point it to, rather than following binding and discovery rules for finding the assembly. This can be useful for loading assemblies used as plug-ins. Be aware, however, that LoadFrom affects performance adversely compared with Load and the lack of version-binding intelligence is a negative for almost all scenarios other than plug-in scenarios.

Once you have obtained a reference to an Assembly object you can discover the types contained within the assembly, as well as create instances and call methods on these types. I'll look at these operations in the next two sections.

Discovery: Finding Code at Run Time

When compiling an application you explicitly or implicitly tell the compiler about code that your application binds to and uses at run time. This code comes in the form of widely reusable class library types such as Object, FileStream, and ArrayList, as well as application-specific types that are packaged in helper assemblies. The compiler stores references to the assemblies that implement these types in your assembly's manifest, and the CLR references the manifest at run time in order to load all the necessary assemblies. This is the typical binding process for managed code.

As I explained previously, late binding also works with code in the form of assemblies; however, the compiler is not directly involved in the binding process. Instead, the code must discover at run time which assemblies it wants to load. This discovery process can be implemented in a variety of ways. As a matter of fact, the .NET Framework does not specify a single discovery method, although it does give you significant tools for implementing your own technique.

Two discovery mechanisms are worthy of mention. The first and simplest (from the software point of view) is to maintain an XML file that documents the code that your application should bind to at run time. The second is a more advanced reflection-based approach that can make for a more useable application. Let's look at the XML approach first.

The XML approach only requires that your code parse the XML file and use the information there to begin loading assemblies. The following example shows a potential XML format:

<PluginAssembly name="MyPlugin.dll"> <Type name="SomeType"/> <Type name="AnotherType"/> </PluginAssembly> <PluginAssembly name="MorePlugins.dll"> </PluginAssembly>

This XML includes the minimum information necessary to use reflection to implement the loading process; that is, the name of an assembly and the name of one or more types in the assembly that you want to activate. All your code must do is find the name of the assembly in the XML and then use code similar to that found in Figure 1 to load the assembly into your AppDomain.

The XML method is easy to implement, but does not make for a useable application. For example, I would not be particularly pleased with a text editor that forced me to edit a .config file of some sort to extend the application. However, for server-side applications this method may be more suitable than using reflection.

The reflection-based discovery method is automated by using a well-known location relative to your application directory. For example, the sample application that I produced for this article searches in a subdirectory named "Plugins" for potential extensibility assemblies (to download the complete source code, go to the link at the top of this article). This particular method is more useable because the user need only copy the assembly files into the file system, and the application will bind to the new code when it launches.

There are two things that make this method more difficult to implement than the XML approach. The first is that you must establish criteria under which assemblies should be loaded. Then you have to reflect over the assembly to discover whether it meets your criteria and should be loaded into your AppDomain.

There are three useful criteria for establishing a type in an assembly as a plug-in for your code. You should employ one of these when using the reflection method for discovery.

Interface criteria Your code can search through an assembly, using reflection, to find all of the types that implement a known interface type.

Base class criteria Your code can search through an assembly, again using reflection, to find all of the types that derive from a known base class.

Custom attribute criteria Finally, custom attributes can be used to mark a type as a plug-in for your application.

In a minute I will show you how to use reflection to discover if a type in a late-bound assembly implements an interface, derives from a base class, or is attributed. But first, let's look at the design trade-offs of using these binding criteria.

The interface and base class criteria are more useful than the custom attribute approach (or any other approach to reflection-based late binding) for two reasons. First, plug-in developers are more likely to be familiar with interfaces or base classes than they are with custom attributes. More importantly, interfaces and base classes can be used to call into the late-bound code in an early-bound manner. For example, when you use an interface as criteria for binding to an object type, you can use an instance of the object through a reference of the interface type. This avoids the need to use reflection to call methods and access other members of the instance, which improves performance and codeability of your extensible application.

To discover information about a type at run time you can use the reflection type System.Type. Each instance of a type derived from Type refers to a type in the system. The Type instance can be used to discover facts about the type at run time such as its base class or the interfaces that it implements.

To obtain a set of types implemented by an assembly, you simply call the GetTypes instance method on an Assembly object. The GetTypes method returns an array of references to Type-derived objects, one for each type defined in the assembly.

To establish whether a type implements a known interface or is derived from a certain base class, use the IsAssignableFrom method of the Type object. The code snippet that follows shows how to test whether or not the type represented by the someType variable implements the IPlugin interface:

Boolean ImplementsPluginInterface(Type someType){ return typeof(IPlugin).IsAssignableFrom(someType); }

This principle also applies to testing for base types.

Loading an assembly to reflect on the types in the assembly works fine for discovering which types you want to plug into your application logic. But what happens if none of them match your criteria? The short answer is that there is no way to unload an assembly that's already in your AppDomain. This is the second reason that a reflection-based discovery method is slightly more difficult to implement than the XML approach.

For certain client applications it may be acceptable to litter your process with all of the assemblies that your code reflects over, even the ones that don't meet your plug-in binding criteria. But for an extensible server application its insufficient to load anything that can't subsequently be unloaded. The solution for this comes in two phases. First, you can unload an assembly so long as you unload the entire AppDomain in which the assembly was loaded. Second, you can programmatically create a temporary AppDomain for the sole purpose of loading assemblies to reflect over to find out whether you want to use the types in the assembly as plug-ins. Once you are past the discovery stage, the temporary AppDomain along with all of its assemblies can be unloaded.

This may sound like a complex solution, but it is in fact a fairly simple piece of code to implement. The PluginManager class published with this article takes this approach, and it takes less than 100 lines of code to implement. If you want to take this approach in your own extensible application, you will find the source for PluginManager useful (see the download).

Activation: Create Instances and Call Methods

Once you have established which types you want to plug into your application, you need to create instances of the objects. This can be done entirely using reflection; however, you will find reflection to be slow, impractical, and difficult to maintain. Alternatively you should use reflection to create instances of plug-in objects, cast the objects to a known interface or base class, and then use the objects in terms of the known type throughout the remainder of its lifetime. Remember that this approach fits hand-in-glove with the interface and base class criteria for discovering plug-in types.

Thanks to the CTS, managed code is object oriented and type safe. The challenge with late-bound code is that an object's type isn't necessarily known at compile time. But thanks to the CTS, you can treat any object as the base class Object and then cast to some base class or interface that's known to your application and the pluggable code. If the object is not compatible with the cast, an InvalidCastException is thrown, which your application can catch and handle accordingly.

However, before you can do any of this, you must create an instance of the object that you are binding to. Unlike early-bound objects, you can't easily use the new keyword to create an instance because the compiler expects the name of a type to be used with new, which of course you don't know for late-bound object types. The solution is the static method Activator.CreateInstance. This method will create an instance of an object given a reference to the object's Type-derived instance, and an optional array of Object references to be used as constructor parameters. The following code uses CreateInstance to create an object and return a well-known interface:

IPlugIn CreatePlugInObject(Type type){ return (IPlugIn) Activator.CreateInstance(type); }

Once you have an object and you have cast it to a known type, then you can call methods on the object like you would any other object through the reference variable. At this point, the objects in your late-bound code are seamlessly integrated with the rest of your application.

Secure Extensibility

If your application will be distributed widely, and anyone might be writing plug-ins, you'll have to think about security. Fortunately, the .NET Framework makes it easy to secure your code. Let's see how.

Imagine again the extensible text editor. In the ideal world, a user should be able to download a plug-in from the Internet and plug it into this application safely, even if it was designed by someone who is not a trusted third party. And when it isn't trusted, it should execute in a partially trusted state where it does not have access to system resources such as the file system or registry.

The .NET Framework can execute partially trusted code through CAS. Because managed code is just-in-time compiled, the CLR has the ability to assert that partially trusted managed code not perform an action for which it lacks permission. If partially trusted code attempts a disallowed action, the CLR throws a SecurityException exception, which the application can catch.

In certain cases code is partially trusted by default, such as controls that are distributed over the Internet and embedded in HTML documents. However, you can also take advantage of CAS so that users can safely use extension assemblies from third parties. In all cases, the CLR considers an assembly to be a unit of security, which means that your application can consist of more than one assembly, each of which might have slightly differing security permissions granted to it. This is perfect for plug-ins.

Again, the .NET Framework makes available many features, and there are a number of approaches that you can take in order to create partially trusted plug-ins. The following steps break down the simplest and most secure approach. First, create two subdirectories for plug-ins to your application. One is for fully trusted plug-ins and the other is home to partially trusted plug-ins. Then programmatically adjust the local security policy to associate a code group with your subdirectory for partially trusted plug-ins. Finally, grant code in the code group Internet permissions, which is to say that it has a subset of permissions deemed safe even for potentially malicious code.

Once you have made this code group, the CLR automatically associates the reduced permissions with any assembly loaded from your partially trusted subdirectory. Aside from having to adjust local security policy (which I will show you how to do shortly), the security for plug-ins works automatically.

Adjusting Security Policy

The .NET Framework security policy engine is very flexible and adjustable. In practice, adjustments to security policy are made manually using the .NET Framework Configuration control panel applet installed with the Framework (see Figure 4). You can also adjust policy programmatically. To write code that modifies security policy, you must start with the SecurityManager type. This helpful type gives you access to the three policy levels installed with the .NET Framework: Enterprise, Machine, and User. I suggest adding your custom code group for plug-ins to the Machine policy level. To find the machine PolicyLevel object use code like that shown in Figure 5.

Figure 5 Finding Machine Policy Level

// Get machine policy static PolicyLevel FindMachinePolicy(){ PolicyLevel ret = null; // Get the policy hierarchy IEnumerator en = SecurityManager.PolicyHierarchy(); en.Reset(); // Find the policy named "Machine" while(en.MoveNext()){ PolicyLevel level = (PolicyLevel) en.Current; if(level.Label == "Machine"){ ret = level; break; } } return ret; }

Figure 4 Security Configuration

Figure 4** Security Configuration **

Code groups are arranged in a logical hierarchy. Once you have obtained a PolicyLevel object, you can traverse the code group hierarchy starting with the code group returned by the PolicyLevel.RootCodeGroup property. The root code group, by default, goes by the name ALL_CODE, which represents all managed code. You should create your custom code group as a child of the ALL_CODE code group.

The code in Figure 6 creates a custom code group for any code loaded from a given URL. The code group has Internet permissions, and also has the PolicyStatementAttribute.Exclusive and PolicyStatementAttribute.LevelFinal bits set to indicate that code that matches this code group only gets these permissions. The URL can be an HTTP, HTTPS, or FILE URL. In order to associate the new code group with a directory on the file system, you can use a file URL that is structured like this: file://d:/programs/extensible-app/partially-trusted.

Figure 6 Creating and Saving Custom Code Group

static void MakePluginCodeGroup( PolicyLevel level, CodeGroup root, String url){ // Get the internet permissiion set PermissionSet permissions = level.GetNamedPermissionSet("Internet"); // Create a membership condition for our path IMembershipCondition membership = new UrlMembershipCondition(url); // Create a policy statement from permissions and condition PolicyStatement statement = new PolicyStatement(permissions, PolicyStatementAttribute.Exclusive | PolicyStatementAttribute.LevelFinal); // New code group UnionCodeGroup group = new UnionCodeGroup(membership, statement); group.Description= "Custom code group for plug-ins"; group.Name = "Custom group"; root.AddChild(group); SecurityManager.SavePolicyLevel(level); }

The CAS features of the .NET Framework are pretty flexible but can take a while to get used to. You should be able to get the information you need to create a plug-in architecture from this article, along with the sample application code that I have included in the download. I do, however, strongly suggest using the .NET Framework Configuration control panel applet to try making changes to your systems policy so that you'll become familiar with the concepts. You can always restore the policy defaults if you go too far.

Extensible Application Design

The ExtensibleApp.exe sample application that comes with this article is an example that supports custom plug-ins. In fact, the application is really just a shell that does nothing but display an MDI window and allow the user to install plug-ins. If you are using the code in this sample as a learning tool for writing your own extensible applications, you should pay special attention to the code in PluginManager.cs. This module contains the PluginManager reusable class that handles all of the non-application-specific plug-in logic for the sample.

Figure 7 Before Plug-in

Figure 7** Before Plug-in **

If you build and run the ExtensibleApp.exe sample, you will see that it lets you select DLLs to install into your application as plug-ins. The sample includes two plug-in projects, PluginProject1.dll and PluginProject2.dll. They take advantage of the API exposed by the application itself to create toolbars and menus, as well as to add a document type to the application. Figure 7 shows the application before plugging in the custom code, and Figure 8 shows the application afterwards.

Figure 8 Running a Plug-in

Figure 8** Running a Plug-in **

The application uses the technologies and techniques discussed so far in this article. Additionally, it shows some approaches to design that you should consider when making your application extensible. Let's take a look at some of these considerations.

Versioning

Versioning is an important aspect of the application lifecycle. It can also have a significant effect on extensible applications. Your extensible application needs to define interfaces, base types, or attribute types that you use for finding and using plug-ins. If you include these types in your regular application EXE or DLL assemblies, then they will be versioned with the rest of your application, which can cause binding hassles for the plug-in assemblies that aren't versioning on the same schedule. But there is a solution.

The solution is that any type you use to discover or bind to plug-ins should be defined in its own assembly where nothing else lives but other plug-in-oriented types. You should also avoid putting any code (or at least not much) in the assembly because you need to version the assembly as infrequently as possible. Meanwhile, the rest of your application can be changed and versioned as needed. Both your application and plug-ins will share the glue assembly, which will only be versioned rarely.

This brings up a question about base types used with plug-ins. Unlike interfaces, base classes commonly contain some code. This is a tough problem to solve, and it is one of the primary reasons that interfaces are the preferred means of calling through to late-bound objects.

However, you should be aware that if you need to version the code in your base class and interface assembly, the .NET Framework does give you the flexibility that you need to redirect binding from the old assembly version to the new version. But this requires a change to binding policy which must be entered either in the app.config file for the application or in the Global Assembly Cache (GAC) for the entire system. Avoiding this eventuality is preferable, so manage your plug-in interfaces and base classes so that they are versioned rarely, if ever.

Robustness

Remember when writing your extensible application that although your code has undergone rigorous quality control, the plug-in code most likely has not. For this reason, any code that calls through an interface or base class reference into late-bound objects should expect the unexpected. Partially trusted plug-ins are likely to throw security exceptions even when the developer wasn't purposefully trying to cause a breach. Similarly, both partially trusted and fully trusted plug-ins are likely to have bugs because the author of the plug-in doesn't have the same inside understanding of your application that you do.

If you are designing your extensible application to support plug-ins written only by you or your team, this may not be a concern. But many extensible applications will want to recover as gracefully as possible when a plug-in object is misbehaving.

One great thing about managed code is that when an object does, in fact, fail, it fails in a well-defined manner—namely, the object throws an exception. So if you are consistent about registering for unhandled exception handlers, and handling expected exceptions around calls into plug-in code, then you can give the user a consistent experience even when a plug-in fails.

One advanced technique that you can use, if you are using interfaces to call through to your plug-in objects, is to wrap all late-bound objects in a proxy object that implements the same interface. This general-purpose proxy object would pass all calls through to the underlying plug-in, but could also wrap calls with exception handlers that are consistent about logging failure, alerting the user of the misbehaving plug-in and other such things. For ultimate plug-in robustness this is a good idea, but for many applications this degree of stability may not be necessary.

Security Considerations

The .NET Framework takes care of security for your partially trusted plug-ins so long as you set policy to restrict the permissions for the assemblies that you load. However, this works great for protecting the system, but does not automatically protect the state of your application. Partially trusted objects share a managed heap with the objects in your application, and consideration needs to be made to limit their access to your internal application objects. Following are some pointers.

Don't unnecessarily designate object types in your application as public objects. If your type is internal (the default case), then they are off limits to partially trusted code in your application. However, public types can sneak into your application without you noticing. For example, the Visual Studio wizard generates public classes when you add a Form-derived type to your project. This isn't necessary for most applications and you should delete the public keyword from these types until you find a need to add it.

Similarly, you should not unnecessarily make members of public types public or protected. Even for nonextensible applications, your members should be private until you find a need to increase their accessibility. Then, if you can get the job done with internal accessibility, do so. Only use protected and public members when you intend to expose the member externally to your assembly.

You should watch for class library types with bad or incomplete security policies. For example, the System.Windows.Forms.MainMenu class exposes a method called GetForm, which returns the form in which the menu lives. This is usually the main form of your application. Even if you did not intend to pass a reference to the main form of your application to a partially trusted plug-in, you might inadvertently allow access by giving plug-ins direct access to Menu-derived objects in the application. The CLR class library developers considered similar security problems. For example, the Form.Parent property demands a security permission of its caller before returning a reference to the parent form. Partially trusted code running with Internet permissions, for example, doesn't have access to this property by default.

As you can see, partially trusted plug-ins may not have access to the general file system or the registry, but you still have to be watchful to keep a malicious plug-in from doing something like closing your application. For client applications this sort of problem is not often critical. For server applications it can be.

In the final section, I'll briefly discuss some advanced possibilities for extensible applications. These topics are too extensive to cover in detail and are not likely to be needed by the majority of applications, but it's helpful to be aware of the possibilities.

Unloadable Plug-in Assemblies

In the section on discovering plug-ins earlier in this article, you might remember the problem of unloading unwanted assemblies. The solution to the problem was to test assemblies for usefullness in a temporary AppDomain so that they can be unloaded if need be. Then, after discovering the assemblies you do want to use, you can load them into your main AppDomain. But what if you don't want all your plug-ins living indefinitely?

For client applications it can be acceptable to load plug-in assemblies, use their types until they are no longer useful, and then ignore the extra assembly for the remainder of the application's lifetime. However, on the server side the requirements are stricter. Server-side applications must be able to run indefinitely without running out of vital resources such as process address space, so you'll need to both load and unload assemblies containing plug-in types.

To do this, you have to discover the assemblies in a temporary application domain as well as use the plugged-in type exclusively from another AppDomain. This adds a level of complexity to your application design, but also nicely separates the plug-ins from your core application logic.

It is possible to instantiate and use objects entirely from within a separate AppDomain, and the feature of the .NET Framework that implements this is called Remoting. Remoting is used to access objects across processes, as well as across the network, but is also used to access objects in different AppDomains even if they are in the same Windows® process. I can't cover Remoting completely here, but you will find extensive coverage in past issues of MSDN® Magazine, and you will also find some simple Remoting code in my sample PluginManager type.

Managed Plug-ins in Unmanaged Apps

With all of these great extensibility features at your disposal, you might feel left out in the cold if yours is a legacy application written in an unmanaged language like Visual Basic® 6.0 or C++. Well, feel left out no more. The CLR itself is a COM object and can be hosted in any Win32 process by any language capable of being a COM client.

This means that you can write your plug-in managed code—in C# or Visual Basic .NET—and then from unmanaged code you can load the runtime and your glue code which, in turn, loads plug-ins that can interact with your unmanaged application in any manner you like. Meanwhile, COM Interop allows you to seamlessly pass COM interfaces back and forth between managed and unmanaged code.

In fact, there are three ways to host and interact with managed code in your unmanaged assembly. You can use COM Interop. The CLR allows you to create COM servers with C#, Visual Basic .NET, and other managed languages. You bind to and use these managed objects from any unmanaged application. You can also use C++ with Managed Extensions (MC++), which is capable of natively mixing managed and unmanaged code in a single process. If your application is written in C++, you might look at Managed C++ for including rich extensibility features. In addition, you can host the CLR directly. The CLR itself is a COM object that you can host. The .NET Framework SDK ships with a C++ header file called MSCorEE.h which includes the necessary definitions to use the runtime as a COM object.

Again, once your unmanaged application is bound to your managed code, your managed code can use the techniques covered in this article to implement extensibility for your application.

Wrapping It Up

The .NET Framework provides some very flexible features for code reflection, late binding, and code security. These features can be mixed and matched in a number of ways to implement extensible applications. When a reliable application is also extensible it's likely to have a longer shelf-life, and may gain something of a following, which can lead to the creation of more plug-ins and wider acceptance. So take these extensibility features for a spin around the block. I think you'll like the results.

For related articles see:
.NET Framework: Building, Packaging, Deploying, and Administering Applications and Types
.NET Framework: Building, Packaging, Deploying, and Administering Applications and Types—Part 2

For background information see:
.NET Framework Security by Brian A. LaMacchia, Sebastian Lange, Matthew Lyons, Rudi Martin, and Kevin T. Price (Addison-Wesley, 2002)

Jason Clark provides training and consulting for Microsoft and Wintellect (https://www.wintellect.com) and is a former developer on the Windows NT and Windows 2000 Server team. He is the coauthor of Programming Server-side Applications for Microsoft Windows 2000 (Microsoft Press, 2000). You can get in touch with Jason at JClark@Wintellect.com.