What is it for?

Extensibilty

The main purpose of this project is to provide extension points to different kinds of applications.
For example, it could be a shell with extensible command set. Shell application can contain only base commands, all other commands can be grouped into separate modules and plugged into shell later, when it will be needed. Shell can provide auto-update services for its modules and can be updated without application restart. Shell user can choose which module to enable and which to disable at runtime, without shell restart.
Another example of extensible application is a website with pluggable modules and widgets. Each module can provide a set of rules for http-request handler selection and a set of widgets, which can be placed in predefined page areas. These modules can be plugged into(and unplugged from) website at runtime, without website restart.

Isolation

Another problem, solved by this project, is module isolation.
Application developers can be sure that extensions(which possibly are developed by 3rd parties) will work only as it was designed by contract and will not interrupt each other.
It is not possible to get direct access to extension's objects or even types from another extension's code.
If an module have too high memory consumption it can be unloaded without any consequences to other modules.
Modules can be loaded with custom permission sets.

Loosely coupled communication

Ideally it would be great(from the maintenance point of view) if there were no communication between extensions, but it is not always possible in real-world applications.
Firebus imposes the loosest type of coupling between extensions - message passing. There are two types of communication in Firebus: event-based asynchronous "fire-and-forget" and synchronous request/response. Extensions don't know about each other's existence, they communicate only through message passing. Considering that extensions are coupled only with message contracts(which should be "unchangable") each extension can be replaced by another one which fits by contract.

How to use it?

In typical extensible application we have plugins which handle requests from the main application, for example lets consider shell application with pluggable commands.

First of all we need to decide, how the host application will communicate with its extensions, and also we need to define message contracts for this communication.
The shell application will send command with set of string arguments to extensions, wait for command completion and show string output in console. This functionality fits well into request/response model of communication, so we need to define contracts for request and response messages. Message contracts should be placed in separate assembly.
Message contract for command request:
[Serializable]
public class CommandRequest
{
	public String Command { get; private set; }

	public IList<String> Arguments { get; private set; }


	public CommandRequest(String command, IEnumerable<String> args)
	{
		if (String.IsNullOrEmpty(command))
		{
			throw new ArgumentNullException("command");
		}
		Command = command;
		Arguments = args == null ? new String[0] : new List<String>(args).ToArray();
	}
}
Message contract for command response:
[Serializable]
public class CommandResponse
{
	public String Output { get; private set; }


	public CommandResponse(String output)
	{
		Output = output ?? String.Empty;
	}
}

Secondly we need to create our extension as IRequestListener<CommandRequest, CommandResponse> implementation. Extension should be placed in separate assembly.
Our file manager extension will support only "dir" command, which prints out the contents of a directory passed in command arguments.
File manager extension implementation:
public class Extension : IRequestListener<CommandRequest, CommandResponse>
{
	public CommandResponse OnRequest(CommandRequest request)
	{
		if (!"dir".Equals(request.Command))
		{
			return new CommandResponse(
				"unknown command");
		}
		var directory = request.Arguments.Count > 0 ? request.Arguments[0] : @"c:\";
		if (!Directory.Exists(directory))
		{
			return new CommandResponse(
				String.Format(
					CultureInfo.CurrentCulture,
					"Directory {0} does not exist.",
					directory));
		}
		var builder = new StringBuilder();
		var files = Directory.GetFiles(directory);
		foreach (var file in files)
		{
			builder.AppendLine(file);
		}
		return new CommandResponse(
			builder.ToString());
	}
}
Now we can cover our file manager extension with unit-tests to make sure that all works as expected. This step is really trivial, so I'll skip it to save your time.

The third and the last step is to create console application, which will instantiate the Host, load extensions and execute commands.
We need to create console application with app.config where we will define our host configuration:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
	<configSections>
		<section name="host" type="FireBus.Core.Configuration.HostConfigurationSection, FireBus.Core, Version=0.1.0.0"/>
	</configSections>
	<host
		messageAccessor="FireBus.Core.ResourcesMessageAccessor, FireBus.Core, Version=0.1.0.0">
		<extensions>
			<extensionToken
				name="FileManager"
				path="..\..\..\Extensions\FireBus.Shell.FileManager\bin\Debug\"
				typeName="FireBus.Shell.FileManager.Extension, FireBus.Shell.FileManager, Version=0.1.0.0"
				/>
		</extensions>
	</host>
</configuration>
Two main things, to pay attention here are the path to folder with extension assemblies and the name of extension type which implements IRequestListener interface.
Next we need to instantiate the host with xml configuration from app.config:
private static Host _Host;

[STAThread]
[LoaderOptimization(LoaderOptimization.MultiDomainHost)]
static void Main()
{
	var config = new XmlConfiguration();
	using (_Host = new Host(config))
	{
		LoadExtensions();
		ReadAndHandle();
	}
}
Load extensions:
private static void LoadExtensions()
{
	_Host.BeginLoadAllExtensions(null, null);
}
Read user input and handle it:
private static void ReadAndHandle()
{
	var text = Read();
	while (text != "exit")
	{
		String command = null;
		var arguments = ParseArgs(text);
		if (arguments.Count > 0)
		{
			command = arguments[0];
			arguments.RemoveAt(0);
		}
		ExecuteCommand(
			command, arguments);

		text = Read();
	}
}

private static String Read()
{
	var text = Console.ReadLine();
	return text;
}

private static IList<String> ParseArgs(String text)
{
	var args = new List<String>(
		text.SplitCommandLine());
	return args;
}

private static void ExecuteCommand(
	String command, IEnumerable<String> args)
{
	var eventArgs = new CommandRequest(
		command,
		args);
	_Host.BeginRequest<CommandRequest, CommandResponse>("FileManager", eventArgs, OnCommandExecuted, null);
}

private static void OnCommandExecuted(IAsyncResult asyncResult)
{
	var response = _Host.EndRequest<CommandResponse>(asyncResult);
	Console.WriteLine(response.Output);
}
You can find full source code in samples solution folder, there this sample is extended with "extension manager" plugin and "help" command.

How does it work?

When client code forces the Host to load an extension from specified folder, the Host creates a new application domain and loads there all the assemblies from extension folder. Then it creates a new type(using Reflection.Emit) for extension adapter, which hides the real extension instance in a private field and inherits from MarshalByRefObject, making it possible to pass the extension instance across the AppDomain boundary by reference. Extension adapter implements all interfaces implemented by real extension, it passes all interface method calls to real extension. It needs to note that extension adapter type is builded and loaded only in extension AppDomain, so when the extension unloads it will unload all the assemblies including extension adapter assembly. The instance of extension adapter is created in extension AppDomain and passed into main AppDomain by reference, where this reference is holded in the Host as a proxy. Extension adapter proxy will be used by host to call extension methods.

When client code forces the Host to notify extensions about an event, the Host iterates through all the extension adapter proxies to find all extensions, which listens to specified type of event. Host notifies all found extensions by calling corresponding method of extension adapter proxy, this call crosses the boundary between AppDomains, so all data passed in the event should be serializable. The same actions are performed when client code tries to make request to an extension.

Each extension can make requests to other extensions or notify them about events, it is even possible to organize communication between extensions without loading message contracts assembly into main application domain! This is possible because all notifications and requests from extension are handled directly in that extension AppDomain(by extension adapter), without crossing boundary with main application domain.

Firebus Architecture
(In SVG)

Last edited Nov 23, 2010 at 4:22 PM by 6opuc, version 14

Comments

No comments yet.