27 December 2011
In this final part of this mini series there are two enhancements I want to add:
eg. be able to apply ITest to the ExampleObject reference:
public interface ITest
{
IEmployee Get(int id);
}
public interface IEmployee
{
int Id { get; }
string Name { get; }
}
public class ExampleObject
{
public object Get(int id)
{
if (id != 1)
throw new ArgumentException("Only id value 1 is supported");
return new Person(1, "Ted");
}
public class Person
{
public Person(int id, string name)
{
if (string.IsNullOrEmpty(name))
throw new ArgumentException("Null/blank name specified");
Id = id;
Name = name.Trim();
}
public int Id { get; private set; }
public string Name { get; private set; }
}
}
The first part is fairly straight-forward so I'll jump straight in -
public class InterfaceApplierFactory : IInterfaceApplierFactory
{
private string _assemblyName;
private bool _createComVisibleClasses;
private Lazy<ModuleBuilder> _moduleBuilder;
public InterfaceApplierFactory(string assemblyName, ComVisibility comVisibilityOfClasses)
{
assemblyName = (assemblyName ?? "").Trim();
if (assemblyName == "")
throw new ArgumentException("Null or empty assemblyName specified");
if (!Enum.IsDefined(typeof(ComVisibility), comVisibilityOfClasses))
throw new ArgumentOutOfRangeException("comVisibilityOfClasses");
_assemblyName = assemblyName;
_createComVisibleClasses = (comVisibilityOfClasses == ComVisibility.Visible);
_moduleBuilder = new Lazy<ModuleBuilder>(
() =>
{
var assemblyBuilder = Thread.GetDomain().DefineDynamicAssembly(
new AssemblyName(_assemblyName),
AssemblyBuilderAccess.Run
);
return assemblyBuilder.DefineDynamicModule(
assemblyBuilder.GetName().Name,
false
);
},
true // isThreadSafe
);
}
public enum ComVisibility
{
Visible,
NotVisible
}
public IInterfaceApplier<T> GenerateInterfaceApplier<T>()
{
if (!typeof(T).IsInterface)
throw new ArgumentException("typeparam must be an interface type");
var typeName = "InterfaceApplier" + Guid.NewGuid().ToString("N");
var typeBuilder = _moduleBuilder.Value.DefineType(
typeName,
TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass |
TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit |
TypeAttributes.AutoLayout,
typeof(object),
new Type[] { typeof(T) }
);
// The content from the previous posts goes here (generating the constructor,
// properties and methods)..
return new InterfaceApplier<T>(
src => (T)Activator.CreateInstance(
typeBuilder.CreateType(),
src
)
);
}
public IInterfaceApplier GenerateInterfaceApplier(Type targetType)
{
var generate = this.GetType().GetMethod("GenerateInterfaceApplier", Type.EmptyTypes);
var generateGeneric = generate.MakeGenericMethod(targetType);
return (IInterfaceApplier)generateGeneric.Invoke(this, new object[0]);
}
private class InterfaceApplier<T> : IInterfaceApplier<T>
{
private Func<object, T> _conversion;
public InterfaceApplier(Func<object, T> conversion)
{
if (!typeof(T).IsInterface)
throw new ArgumentException("Invalid typeparam - must be an interface");
if (conversion == null)
throw new ArgumentNullException("conversion");
_conversion = conversion;
}
public Type TargetType
{
get { return typeof(T); }
}
public T Apply(object src)
{
return _conversion(src);
}
object IInterfaceApplier.Apply(object src)
{
return Apply(src);
}
}
}
public interface IInterfaceApplierFactory
{
IInterfaceApplier<T> GenerateInterfaceApplier<T>();
IInterfaceApplier GenerateInterfaceApplier(Type targetType);
}
public interface IInterfaceApplier<T> : IInterfaceApplier
{
new T Apply(object src);
}
public interface IInterfaceApplier
{
Type TargetType { get; }
object Apply(object src);
}
Using this class means that we only need one instance of the ModuleBuilder no matter how many interfaces we're wrapping around objects and an "IInterfaceApplier" is returned instead of a reference to the interface-wrapped object. Note that I've used the .Net 4.0 Lazy class to instantiate the ModuleBuilder only the first time that it's required, but if you're using an earlier version of .Net then this could be replaced with the implementation (see my previous post about this here) or even by instantiating it directly from within the constructor.
I've also supported an alternative method signature for GenerateInterfaceApplier such that the target interface can be specified as an argument rather than a typeparam to a generic method - this will become important in the next section and the only interesting things to note are how IInterfaceApplier<T> is returned from the generic method as opposed to the IInterfaceApplier returned from the typeparam-less signature and how the alternate method calls into the typeparam'd version using reflection.
The approach I'm going to use here is to introduce a new interface that will be used when generating the interface appliers -
public interface IReadValueConverter
{
object Convert(PropertyInfo property, object value);
object Convert(MethodInfo method, object value);
}
Values will be passed through this when returned by property getters or (non-void) methods and it wil be responsible for ensuring that the value returned from the Convert method matches the property.PropertyType / method.ReturnType.
This will mean we'll change the method signature to:
public InterfaceApplier<T> GenerateInterfaceApplier<T>(IReadValueConverter readValueConverter)
That we'll change the constructor on the generated type:
// Declare private fields
var srcField = typeBuilder.DefineField("_src", typeof(object), FieldAttributes.Private);
var readValueConverterField = typeBuilder.DefineField(
"_readValueConverter",
typeof(IReadValueConverter),
FieldAttributes.Private
);
// Generate: base.ctor()
var ctorBuilder = typeBuilder.DefineConstructor(
MethodAttributes.Public,
CallingConventions.Standard,
new[] { typeof(object) }
);
var ilCtor = ctorBuilder.GetILGenerator();
ilCtor.Emit(OpCodes.Ldarg_0);
ilCtor.Emit(OpCodes.Call, typeBuilder.BaseType.GetConstructor(Type.EmptyTypes));
// Generate: if (src != null), don't throw exception
var nonNullSrcArgumentLabel = ilCtor.DefineLabel();
ilCtor.Emit(OpCodes.Ldarg_1);
ilCtor.Emit(OpCodes.Brtrue, nonNullSrcArgumentLabel);
ilCtor.Emit(OpCodes.Ldstr, "src");
ilCtor.Emit(
OpCodes.Newobj,
typeof(ArgumentNullException).GetConstructor(new[] { typeof(string) })
);
ilCtor.Emit(OpCodes.Throw);
ilCtor.MarkLabel(nonNullSrcArgumentLabel);
// Generate: if (readValueConverter != null), don't throw exception
var nonNullReadValueConverterArgumentLabel = ilCtor.DefineLabel();
ilCtor.Emit(OpCodes.Ldarg_2);
ilCtor.Emit(OpCodes.Brtrue, nonNullReadValueConverterArgumentLabel);
ilCtor.Emit(OpCodes.Ldstr, "readValueConverter");
ilCtor.Emit(
OpCodes.Newobj,
typeof(ArgumentNullException).GetConstructor(new[] { typeof(string) })
);
ilCtor.Emit(OpCodes.Throw);
ilCtor.MarkLabel(nonNullReadValueConverterArgumentLabel);
// Generate: this._src = src
ilCtor.Emit(OpCodes.Ldarg_0);
ilCtor.Emit(OpCodes.Ldarg_1);
ilCtor.Emit(OpCodes.Stfld, srcField);
// Generate: this._readValueConverter = readValueConverter
ilCtor.Emit(OpCodes.Ldarg_0);
ilCtor.Emit(OpCodes.Ldarg_2);
ilCtor.Emit(OpCodes.Stfld, readValueConverterField);
// All done
ilCtor.Emit(OpCodes.Ret);
That we'll change the reading of properties:
// Define get method, if required
if (property.CanRead)
{
var getFuncBuilder = typeBuilder.DefineMethod(
"get_" + property.Name,
MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot |
MethodAttributes.SpecialName | MethodAttributes.Virtual | MethodAttributes.Final,
property.PropertyType,
Type.EmptyTypes
);
// Generate: return this._readValueConverter.Convert(
// property.DeclaringType.GetProperty(property.Name)
// _src.GetType().InvokeMember(property.Name, BindingFlags.GetProperty, null, _src, null)
// );
var ilGetFunc = getFuncBuilder.GetILGenerator();
ilGetFunc.Emit(OpCodes.Ldarg_0);
ilGetFunc.Emit(OpCodes.Ldfld, readValueConverterField);
ilGetFunc.Emit(OpCodes.Ldtoken, property.DeclaringType);
ilGetFunc.Emit(
OpCodes.Call,
typeof(Type).GetMethod("GetTypeFromHandle", new[] { typeof(RuntimeTypeHandle) })
);
ilGetFunc.Emit(OpCodes.Ldstr, property.Name);
ilGetFunc.Emit(
OpCodes.Call,
typeof(Type).GetMethod("GetProperty", new[] { typeof(string) })
);
ilGetFunc.Emit(OpCodes.Ldarg_0);
ilGetFunc.Emit(OpCodes.Ldfld, srcField);
ilGetFunc.Emit(OpCodes.Callvirt, typeof(Type).GetMethod("GetType", Type.EmptyTypes));
ilGetFunc.Emit(OpCodes.Ldstr, property.Name);
ilGetFunc.Emit(OpCodes.Ldc_I4, (int)BindingFlags.GetProperty);
ilGetFunc.Emit(OpCodes.Ldnull);
ilGetFunc.Emit(OpCodes.Ldarg_0);
ilGetFunc.Emit(OpCodes.Ldfld, srcField);
ilGetFunc.Emit(OpCodes.Ldnull);
ilGetFunc.Emit(OpCodes.Callvirt, methodInfoInvokeMember);
ilGetFunc.Emit(
OpCodes.Callvirt,
typeof(IReadValueConverter).GetMethod(
"Convert",
new[] { typeof(PropertyInfo), typeof(object) }
)
);
if (property.PropertyType.IsValueType)
ilGetFunc.Emit(OpCodes.Unbox_Any, property.PropertyType);
ilGetFunc.Emit(OpCodes.Ret);
propBuilder.SetGetMethod(getFuncBuilder);
}
And that we'll change the calling of methods:
// .. skipped out the first half of the method-generating code
// - see http://www.productiverage.com/Read/15
// Generate either:
// _src.GetType().InvokeMember(method.Name, BindingFlags.InvokeMethod, null, _src, args);
// or
// return this._readValueConverter.Convert(
// method.DeclaringType.GetMethod(method.Name, {MethodArgTypes})
// this._src.GetType().InvokeMember(
// property.Name,
// BindingFlags.InvokeMethod,
// null,
// _src,
// null
// )
// );
if (!method.ReturnType.Equals(typeof(void)))
{
// We only need to use the readValueConverter if returning a value
// Generate: Type[] argTypes
var argTypes = ilFunc.DeclareLocal(typeof(Type[]));
// Generate: argTypes = new Type[x]
ilFunc.Emit(OpCodes.Ldc_I4, parameters.Length);
ilFunc.Emit(OpCodes.Newarr, typeof(Type));
ilFunc.Emit(OpCodes.Stloc_1);
for (var index = 0; index < parameters.Length; index++)
{
// Generate: argTypes[n] = ..;
var parameter = parameters[index];
ilFunc.Emit(OpCodes.Ldloc_1);
ilFunc.Emit(OpCodes.Ldc_I4, index);
ilFunc.Emit(OpCodes.Ldtoken, parameters[index].ParameterType);
ilFunc.Emit(OpCodes.Stelem_Ref);
}
// Will call readValueConverter.Convert, passing MethodInfo reference before value
ilFunc.Emit(OpCodes.Ldarg_0);
ilFunc.Emit(OpCodes.Ldfld, readValueConverterField);
ilFunc.Emit(OpCodes.Ldtoken, method.DeclaringType);
ilFunc.Emit(
OpCodes.Call,
typeof(Type).GetMethod("GetTypeFromHandle", new[] { typeof(RuntimeTypeHandle) })
);
ilFunc.Emit(OpCodes.Ldstr, method.Name);
ilFunc.Emit(OpCodes.Ldloc_1);
ilFunc.Emit(
OpCodes.Call,
typeof(Type).GetMethod("GetMethod", new[] { typeof(string), typeof(Type[]) })
);
}
ilFunc.Emit(OpCodes.Ldarg_0);
ilFunc.Emit(OpCodes.Ldfld, srcField);
ilFunc.Emit(OpCodes.Callvirt, typeof(Type).GetMethod("GetType", Type.EmptyTypes));
ilFunc.Emit(OpCodes.Ldstr, method.Name);
ilFunc.Emit(OpCodes.Ldc_I4, (int)BindingFlags.InvokeMethod);
ilFunc.Emit(OpCodes.Ldnull);
ilFunc.Emit(OpCodes.Ldarg_0);
ilFunc.Emit(OpCodes.Ldfld, srcField);
ilFunc.Emit(OpCodes.Ldloc_0);
ilFunc.Emit(OpCodes.Callvirt, methodInfoInvokeMember);
if (method.ReturnType.Equals(typeof(void)))
ilFunc.Emit(OpCodes.Pop);
else
{
ilFunc.Emit(
OpCodes.Callvirt,
typeof(IReadValueConverter).GetMethod(
"Convert",
new[] { typeof(MethodInfo), typeof(object) }
)
);
if (method.ReturnType.IsValueType)
ilFunc.Emit(OpCodes.Unbox_Any, method.ReturnType);
}
ilFunc.Emit(OpCodes.Ret);
A naive implementation might be as follows:
public class SimpleReadValueConverter : IReadValueConverter
{
private IInterfaceApplierFactory _interfaceApplierFactory;
public SimpleReadValueConverter(IInterfaceApplierFactory interfaceApplierFactory)
{
if (interfaceApplierFactory == null)
throw new ArgumentNullException("interfaceApplierFactory");
_interfaceApplierFactory = interfaceApplierFactory;
}
public object Convert(PropertyInfo property, object value)
{
if (property == null)
throw new ArgumentNullException("property");
return tryToConvertValueIfRequired(property.PropertyType, value);
}
public object Convert(MethodInfo method, object value)
{
if (method == null)
throw new ArgumentNullException("method");
return tryToConvertValueIfRequired(method.ReturnType, value);
}
private object tryToConvertValueIfRequired(Type targetType, object value)
{
if (targetType == null)
throw new ArgumentNullException("targetType");
// If no conversion is required, no work to do
// - Note: We can only deal with applying interfaces to objects so if a conversion
// is required where the target is not an interface then there's nothing we can do
// here, we'll have to return the value unconverted (likewise, if the target type
// is an int but the current value is null, although this is obviously incorrect
// there's nothing we can do about it here)
if (!targetType.IsInterface || (value == null)
|| (value.GetType().IsSubclassOf(targetType)))
return value;
return _interfaceApplierFactory.GenerateInterfaceApplier(targetType, this)
.Apply(value);
}
}
This will do the job but it jumps out at me that if the same interface needs to be applied to multiple return values (ie. from different properties or methods) then the work done to generate that interface applier will be repeated for each request. It might be better (require less memory and cpu resources) to build up a list of interfaces that have already been handled and re-use the interface appliers where possible -
public class CachedReadValueConverter : IReadValueConverter
{
private IInterfaceApplierFactory _interfaceApplierFactory;
private NonNullImmutableList<IInterfaceApplier> _interfaceAppliers;
private object _writeLock;
public CachedReadValueConverter(IInterfaceApplierFactory interfaceApplierFactory)
{
if (interfaceApplierFactory == null)
throw new ArgumentNullException("interfaceApplierFactory");
_interfaceApplierFactory = interfaceApplierFactory;
_interfaceAppliers = new NonNullImmutableList<IInterfaceApplier>();
_writeLock = new object();
}
public object Convert(PropertyInfo property, object value)
{
if (property == null)
throw new ArgumentNullException("property");
return tryToConvertValueIfRequired(property.PropertyType, value);
}
public object Convert(MethodInfo method, object value)
{
if (method == null)
throw new ArgumentNullException("method");
return tryToConvertValueIfRequired(method.ReturnType, value);
}
private object tryToConvertValueIfRequired(Type targetType, object value)
{
if (targetType == null)
throw new ArgumentNullException("targetType");
// If no conversion is required, no work to do
// - Note: We can only deal with applying interfaces to objects so if a conversion
// is required where the target is not an interface then there's nothing we can
// do here so we'll have to return the value unconverted (likewise, if the target
// type is an int but the current value is null, although this is obviously
// incorrect but there's nothing we can do about it here)
if (!targetType.IsInterface || (value == null)
|| (value.GetType().IsSubclassOf(targetType)))
return value;
// Do we already have an interface applier available for this type?
var interfaceApplierExisting = _interfaceAppliers.FirstOrDefault(
i => i.TargetType.Equals(targetType)
);
if (interfaceApplierExisting != null)
return interfaceApplierExisting.Apply(value);
// Try to generate new interface applier
var interfaceApplierNew = _interfaceApplierFactory.GenerateInterfaceApplier(
targetType,
this
);
lock (_writeLock)
{
if (!_interfaceAppliers.Any(i => i.TargetType.Equals(targetType)))
_interfaceAppliers = _interfaceAppliers.Add(interfaceApplierNew);
}
return interfaceApplierNew.Apply(value);
}
}
There will still be cases where there have to be multiple interface appliers for a given interface if there are interrelated references but this should limit how many duplicates are generated. For example:
using COMInteraction.InterfaceApplication;
using COMInteraction.InterfaceApplication.ReadValueConverters;
namespace Tester
{
class Program
{
static void Main(string[] args)
{
var n1 = new Node() { Name = "Node1" };
var n2 = new Node() { Name = "Node2" };
var n3 = new Node() { Name = "Node3" };
n1.Next = n2;
n2.Previous = n1;
n2.Next = n3;
n3.Previous = n2;
var interfaceApplierFactory = new InterfaceApplierFactory(
"DynamicAssembly",
InterfaceApplierFactory.ComVisibility.NotVisible
);
var interfaceApplier = interfaceApplierFactory.GenerateInterfaceApplier<INode>(
new CachedReadValueConverter(interfaceApplierFactory)
);
var n2Wrapped = interfaceApplier.Apply(n2);
}
public class Node
{
public string Name { get; set; }
public Node Previous { get; set; }
public Node Next { get; set; }
}
}
public interface INode
{
string Name { get; set; }
INode Previous { get; set; }
INode Next { get; set; }
}
}
Each unique interface applier has a specific class name (of the form "InterfaceApplier{0}" where {0} is a guid). Examining the properties on n2Wrapped you can see that the class for n2Wrapped is different to the class for its Next and Previous properties as the INode interface applier hadn't been completely generated against n2 before an INode interface applier was required for these properties. But after this, all further INode-wrapped instances will share the same interface applier as the Previous and Next properties received - so there will be one "wasted" generated class but that's still a better job than the SimpleReadValueConverter would have managed.
In the above example, the INode interface has to be located outside of the Program class since it must be accessible by the InterfaceApplierFactory and the Program class is private. This isn't an issue for the Node class as the InterfaceApplierFactory doesn't need direct access to that, it just returns an IInterfaceApplier that we pass the n2 reference to as an argument. Alternatively, the Program class could be made public. This isn't exactly rocket science but if it slips your mind then you're presented with an error such as -
TypeLoadException: "Type
'InterfaceApplierdb2cb792e09d424a8dcecbeca6276dc8'
from assembly'DynamicAssembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'
is attempting to implement an inaccessible interface."
at the line
return new InterfaceApplier<T>(
src => (T)Activator.CreateInstance(
typeBuilder.CreateType(),
src,
readValueConverter
)
);
in InterfaceApplierFactory, which isn't the friendliest of warnings!
Posted at 18:38
14 December 2011
Today, I'm going to address some of the "future developments" I left at the end of the last post. Specifically:
Part one is easy. Take the code from the last article and wrap in
public static T WrapObject<T>(object src)
{
if (src == null)
throw new ArgumentNullException("src");
if (!typeof(T).IsInterface)
throw new ArgumentException("Typeparam T must be an interface type");
// Insert existing code that generates the new class with its constructor, properties
// and methods here..
return (T)Activator.CreateInstance(
typeBuilder.CreateType(),
src
);
}
Ta-da! Note that we ensure that the typeparam T really is an interface - we made assumptions about this in the last article, so we need to assert this fact here. (It means that we only ever have to deal with properties and methods, and that they will always be public).
This part is not much more difficult. We'll introduce something to recursively trawl through any interfaces that the target interface implements and build a list of them all:
public class InterfaceHierarchyCombiner
{
private Type _targetInterface;
private List<Type> _interfaces;
public InterfaceHierarchyCombiner(Type targetInterface)
{
if (targetInterface == null)
throw new ArgumentNullException("targetInterface");
if (!targetInterface.IsInterface)
throw new ArgumentException("targetInterface must be an interface type", "targetInterface");
_interfaces = new List<Type>();
buildInterfaceInheritanceList(targetInterface, _interfaces);
_targetInterface = targetInterface;
}
private static void buildInterfaceInheritanceList(Type targetInterface, List<Type> types)
{
if (targetInterface == null)
throw new ArgumentNullException("targetInterface");
if (!targetInterface.IsInterface)
throw new ArgumentException("targetInterface must be an interface type", "targetInterface");
if (types == null)
throw new ArgumentNullException("types");
if (!types.Contains(targetInterface))
types.Add(targetInterface);
foreach (var inheritedInterface in targetInterface.GetInterfaces())
{
if (!types.Contains(inheritedInterface))
{
types.Add(inheritedInterface);
buildInterfaceInheritanceList(inheritedInterface, types);
}
}
}
public Type TargetInterface
{
get { return _targetInterface; }
}
public IEnumerable<Type> Interfaces
{
get { return _interfaces.AsReadOnly(); }
}
}
Then, in this new WrapObject method, we call instantiate a new InterfaceHierarchyCombiner for the typeparam T and use retrieve all the properties and methods from the Interfaces list, rather than just those on T.
eg. Instead of
foreach (var property in typeof(ITest).GetProperties())
{
// Deal with the properties..
we consider
var interfaces = (new InterfaceHierarchyCombiner(typeof(T))).Interfaces;
foreach (var property in interfaces.SelectMany(i => i.GetProperties()))
{
// Deal with the properties..
and likewise for the methods.
It's worth noting that there may be multiple methods within the interface hierarchy with the same name and signature. It may be worth keeping track of which properties / methods have had corresponding IL generated but - other than generating more instructions in the loop than strictly necessary - it doesn't do any harm generating duplicate properties or methods (so I haven't worried about it for now).
What I wanted to do for the wrappers I was implementing was to create classes with the [ComVisible(true)] and [ClassInterface(ClassInterface.None)] attributes. This is achieved by specifying these attributes on the typeBuilder (as seen in the code in the last article):
typeBuilder.SetCustomAttribute(
new CustomAttributeBuilder(
typeof(ComVisibleAttribute).GetConstructor(new[] { typeof(bool) }),
new object[] { true }
)
);
typeBuilder.SetCustomAttribute(
new CustomAttributeBuilder(
typeof(ClassInterfaceAttribute).GetConstructor(new[] { typeof(ClassInterfaceType) }),
new object[] { ClassInterfaceType.None }
)
);
Again, easy! Once you know how :)
I've not included a complete sample here since it would take up a fairly hefty chunk of space but also because I created a GitHub repository with the final code. This can be found at https://github.com/ProductiveRage/COMInteraction
This includes the work here but also the work I want to address next time; a way to automagically wrap return values where required and a way to change the WrapObject method so that if the same interface is to be applied to multiple objects, only a single call is required (and an object be returned that can wrap any given reference in that interface). The example I put out for this return-value-wrapping was that we want to wrap an object in ITest and have the return value from its Get method also be wrapped if it did not already implement IEmployee:
public interface ITest
{
IEmployee Get(int id);
}
public interface IEmployee
{
int Id { get; }
string Name { get; }
}
For more details of how exactly this will all work, I'll see you back here; same bat-time, same bat-channel! :)
Posted at 21:16
13 December 2011
Another area of this migration proof-of-concept work I'm doing at the day job involves investigating the best way to swap out a load of COM components for C# versions over time. The plan initially is to define interfaces for them and code against those interfaces, write wrapper classes around the components that implement these interfaces and one day rewrite them one-by-one.
Writing the interfaces is valuable since it enables some documentation-through-comments to be generated for each method and property and forces me to look into the idiosyncracies of the various components.
However, writing endless wrappers for the components to "join" them to the interfaces sounded boring! Even if I used the .Net 4.0 "dynamic" keyword it seemed like there'd be a lot of repetition and opportunity for me to mistype a property name and not realise until debugging / writing tests. (Plus I found a problem that prevented me from using "dynamic" with the WSCs I was wrapping - see the bottom of this post for more details).
I figured this is the sort of thing that should be dynamically generatable from the interfaces instead of writing them all by hand - something like how Moq can create generate Mock<ITest> implementations. I did most of this investigation back in Summer, not long after the AutoMapper work I was looking into, and had hoped I'd be able to leverage my sharpened Linq Expression dynamic code generation skills. Alas, it seems that new classes can not be defined in this manner so I had to go deeper..
I was aware that IL could be generated by code at runtime and executed as any other other loaded assembly might be. I'd read (and chuckled at) this article in the past but never taken it any further: Dynamic... But Fast: The Tale of Three Monkeys, A Wolf and the DynamicMethod and ILGenerator Classes
As I tried to find out more information, though, it seemed that a lot of articles would make the point that you could find out how to construct IL by using the IL Disassembler that's part of the .Net SDK: ildasm.exe (located in C:\Program Files\Microsoft SDKs\Windows\v7.0A\bin on my computer). This makes sense because once you start constructing simple classes and examining the generated code in ildasm you can start to get a reasonable idea for how to write the generation code yourself. But it still took me quite a while to get to the point where the following worked!
What I really wanted was something to take, for example:
public interface ITest
{
int GetValue(string id);
string Name { get; set; }
}
and wrap an object that had that method and property such that the interface was exposed - eg.
public class TestWrapper : ITest
{
private object _src;
public TestWrapper(object src)
{
if (src == null)
throw new ArgumentNullException("src");
_src = src;
}
public int GetValue(string id)
{
return _src.GetType().InvokeMember(
"GetValue",
BindingFlags.InvokeMethod,
null,
_src,
new object[] { id }
);
}
public string Name
{
get
{
return _src.GetType().InvokeMember("Name", BindingFlags.GetProperty, null, _src, null)
}
set
{
_src.GetType().InvokeMember("Name", BindingFlags.SetProperty, null, _src, new object[] { value });
}
}
}
It may seem like using reflection will result in there being overhead in the calls but the primary objective was to wrap a load of WSCs in C# interfaces so they could be rewritten later while doing the job for now - so performance wasn't really a massive concern at this point.
The first thing to be aware of is that we can't create new classes in the current assembly, we'll have to create them in a new one. So we start off with
var assemblyBuilder = Thread.GetDomain().DefineDynamicAssembly(
new AssemblyName("DynamicAssembly"), // This is not a magic string, it can be called anything
AssemblyBuilderAccess.Run
);
var moduleBuilder = assemblyBuilder.DefineDynamicModule(
assemblyBuilder.GetName().Name,
false
);
// This NewGuid call is just to get a unique name for the new construct
var typeName = "InterfaceApplier" + Guid.NewGuid().ToString();
var typeBuilder = moduleBuilder.DefineType(
typeName,
TypeAttributes.Public
| TypeAttributes.Class
| TypeAttributes.AutoClass
| TypeAttributes.AnsiClass
| TypeAttributes.BeforeFieldInit
| TypeAttributes.AutoLayout,
typeof(object),
new Type[] { typeof(ITest) }
);
The TypeAttribute values I copied from the ildasm output I examined.
Note that we're specifying ITest as the interface we're implementing by passing it as the "interfaces" parameter to the moduleBuilder's DefineType method.
The constructor is fairly straight forward. The thing that took me longest to wrap my head around was how to form the "if (src == null) throw new ArgumentNullException()" construct. If seems that this is most easily done by declaring a label to jump to if src is not null which allows execution to leap over the point at which an ArgumentNullException will be raised.
// Declare private _src field
var srcField = typeBuilder.DefineField("_src", typeof(object), FieldAttributes.Private);
var ctorBuilder = typeBuilder.DefineConstructor(
MethodAttributes.Public,
CallingConventions.Standard,
new[] { typeof(object) }
);
// Generate: base.ctor()
var ilCtor = ctorBuilder.GetILGenerator();
ilCtor.Emit(OpCodes.Ldarg_0);
ilCtor.Emit(OpCodes.Call, typeBuilder.BaseType.GetConstructor(Type.EmptyTypes));
// Generate: if (src != null), don't throw new ArgumentException("src")
var nonNullSrcArgumentLabel = ilCtor.DefineLabel();
ilCtor.Emit(OpCodes.Ldarg_1);
ilCtor.Emit(OpCodes.Brtrue, nonNullSrcArgumentLabel);
ilCtor.Emit(OpCodes.Ldstr, "src");
ilCtor.Emit(OpCodes.Newobj, typeof(ArgumentNullException).GetConstructor(new[] { typeof(string) }));
ilCtor.Emit(OpCodes.Throw);
ilCtor.MarkLabel(nonNullSrcArgumentLabel);
// Generate: _src = src
ilCtor.Emit(OpCodes.Ldarg_0);
ilCtor.Emit(OpCodes.Ldarg_1);
ilCtor.Emit(OpCodes.Stfld, srcField);
// All done!
ilCtor.Emit(OpCodes.Ret);
Although there's only a single property in the ITest example we're looking at, we might as look ahead and loop over all properties the interface has so we can apply the same sort of code to other interfaces. Since we are dealing with interfaces, we only need to consider whether a property is gettable, settable or both - there's no public / internal / protected / private / etc.. to worry about. Likewise, we only have to worry about properties and methods - interfaces can't declare fields.
foreach (var property in typeof(ITest).GetProperties())
{
var methodInfoInvokeMember = typeof(Type).GetMethod(
"InvokeMember",
new[]
{
typeof(string),
typeof(BindingFlags),
typeof(Binder),
typeof(object),
typeof(object[])
}
);
// Prepare the property we'll add get and/or set accessors to
var propBuilder = typeBuilder.DefineProperty(
property.Name,
PropertyAttributes.None,
property.PropertyType,
Type.EmptyTypes
);
// Define get method, if required
if (property.CanRead)
{
var getFuncBuilder = typeBuilder.DefineMethod(
"get_" + property.Name,
MethodAttributes.Public
| MethodAttributes.HideBySig
| MethodAttributes.NewSlot
| MethodAttributes.SpecialName
| MethodAttributes.Virtual
| MethodAttributes.Final,
property.PropertyType,
Type.EmptyTypes
);
// Generate:
// return _src.GetType().InvokeMember(property.Name, BindingFlags.GetProperty, null, _src, null)
var ilGetFunc = getFuncBuilder.GetILGenerator();
ilGetFunc.Emit(OpCodes.Ldarg_0);
ilGetFunc.Emit(OpCodes.Ldfld, srcField);
ilGetFunc.Emit(OpCodes.Callvirt, typeof(Type).GetMethod("GetType", Type.EmptyTypes));
ilGetFunc.Emit(OpCodes.Ldstr, property.Name);
ilGetFunc.Emit(OpCodes.Ldc_I4, (int)BindingFlags.GetProperty);
ilGetFunc.Emit(OpCodes.Ldnull);
ilGetFunc.Emit(OpCodes.Ldarg_0);
ilGetFunc.Emit(OpCodes.Ldfld, srcField);
ilGetFunc.Emit(OpCodes.Ldnull);
ilGetFunc.Emit(OpCodes.Callvirt, methodInfoInvokeMember);
if (property.PropertyType.IsValueType)
ilGetFunc.Emit(OpCodes.Unbox_Any, property.PropertyType);
ilGetFunc.Emit(OpCodes.Ret);
propBuilder.SetGetMethod(getFuncBuilder);
}
// Define set method, if required
if (property.CanWrite)
{
var setFuncBuilder = typeBuilder.DefineMethod(
"set_" + property.Name,
MethodAttributes.Public
| MethodAttributes.HideBySig
| MethodAttributes.SpecialName
| MethodAttributes.Virtual,
null,
new Type[] { property.PropertyType }
);
var valueParameter = setFuncBuilder.DefineParameter(1, ParameterAttributes.None, "value");
var ilSetFunc = setFuncBuilder.GetILGenerator();
// Generate:
// _src.GetType().InvokeMember(
// property.Name, BindingFlags.SetProperty, null, _src, new object[1] { value }
// );
// Note: Need to declare assignment of local array to pass to InvokeMember (argValues)
var argValues = ilSetFunc.DeclareLocal(typeof(object[]));
ilSetFunc.Emit(OpCodes.Ldarg_0);
ilSetFunc.Emit(OpCodes.Ldfld, srcField);
ilSetFunc.Emit(OpCodes.Callvirt, typeof(Type).GetMethod("GetType", Type.EmptyTypes));
ilSetFunc.Emit(OpCodes.Ldstr, property.Name);
ilSetFunc.Emit(OpCodes.Ldc_I4, (int)BindingFlags.SetProperty);
ilSetFunc.Emit(OpCodes.Ldnull);
ilSetFunc.Emit(OpCodes.Ldarg_0);
ilSetFunc.Emit(OpCodes.Ldfld, srcField);
ilSetFunc.Emit(OpCodes.Ldc_I4_1);
ilSetFunc.Emit(OpCodes.Newarr, typeof(Object));
ilSetFunc.Emit(OpCodes.Stloc_0);
ilSetFunc.Emit(OpCodes.Ldloc_0);
ilSetFunc.Emit(OpCodes.Ldc_I4_0);
ilSetFunc.Emit(OpCodes.Ldarg_1);
if (property.PropertyType.IsValueType)
ilSetFunc.Emit(OpCodes.Box, property.PropertyType);
ilSetFunc.Emit(OpCodes.Stelem_Ref);
ilSetFunc.Emit(OpCodes.Ldloc_0);
ilSetFunc.Emit(OpCodes.Callvirt, methodInfoInvokeMember);
ilSetFunc.Emit(OpCodes.Pop);
ilSetFunc.Emit(OpCodes.Ret);
propBuilder.SetSetMethod(setFuncBuilder);
}
}
The gist is that for the getter and/or setter, we have to declare a method and then assign that method to be the GetMethod or SetMethod for the property. The method is named by prefixing the property name with either "get_" or "set_", as is consistent with how C# generates it class properties' IL.
The call to
_src.GetType().InvokeMember(
property.Name,
BindingFlags.SetProperty,
null,
_src,
new object[] { value }
);
is a bit painful as we have to declare an array with a single element to pass to the method, where that single element is the "value" reference available within the setter.
Also worthy of note is that when returning a ValueType or setting a ValueType. As we're expecting to either return an object or set an object, the value has to be "boxed" otherwise bad things will happen!
Like the TypeAttributes in the Constructor, the MethodAttributes I've applied here were gleaned from looking at IL generated by Visual Studio.
We're on the home stretch now! Methods are very similar to the property setters except that we may have zero, one or multiple parameters to handle and we may or may not (if the return type is void) return a value from the method.
foreach (var method in typeof(ITest).GetMethods())
{
var parameters = method.GetParameters();
var parameterTypes = new List<Type>();
foreach (var parameter in parameters)
{
if (parameter.IsOut)
throw new ArgumentException("Output parameters are not supported");
if (parameter.IsOptional)
throw new ArgumentException("Optional parameters are not supported");
if (parameter.ParameterType.IsByRef)
throw new ArgumentException("Ref parameters are not supported");
parameterTypes.Add(parameter.ParameterType);
}
var funcBuilder = typeBuilder.DefineMethod(
method.Name,
MethodAttributes.Public
| MethodAttributes.HideBySig
| MethodAttributes.NewSlot
| MethodAttributes.Virtual
| MethodAttributes.Final,
method.ReturnType,
parameterTypes.ToArray()
);
var ilFunc = funcBuilder.GetILGenerator();
// Generate: object[] args
var argValues = ilFunc.DeclareLocal(typeof(object[]));
// Generate: args = new object[x]
ilFunc.Emit(OpCodes.Ldc_I4, parameters.Length);
ilFunc.Emit(OpCodes.Newarr, typeof(Object));
ilFunc.Emit(OpCodes.Stloc_0);
for (var index = 0; index < parameters.Length; index++)
{
// Generate: args[n] = ..;
var parameter = parameters[index];
ilFunc.Emit(OpCodes.Ldloc_0);
ilFunc.Emit(OpCodes.Ldc_I4, index);
ilFunc.Emit(OpCodes.Ldarg, index + 1);
if (parameter.ParameterType.IsValueType)
ilFunc.Emit(OpCodes.Box, parameter.ParameterType);
ilFunc.Emit(OpCodes.Stelem_Ref);
}
var methodInfoInvokeMember = typeof(Type).GetMethod(
"InvokeMember",
new[]
{
typeof(string),
typeof(BindingFlags),
typeof(Binder),
typeof(object),
typeof(object[])
}
);
// Generate:
// [return] _src.GetType().InvokeMember(method.Name, BindingFlags.InvokeMethod, null, _src, args);
ilFunc.Emit(OpCodes.Ldarg_0);
ilFunc.Emit(OpCodes.Ldfld, srcField);
ilFunc.Emit(OpCodes.Callvirt, typeof(Type).GetMethod("GetType", Type.EmptyTypes));
ilFunc.Emit(OpCodes.Ldstr, method.Name);
ilFunc.Emit(OpCodes.Ldc_I4, (int)BindingFlags.InvokeMethod);
ilFunc.Emit(OpCodes.Ldnull);
ilFunc.Emit(OpCodes.Ldarg_0);
ilFunc.Emit(OpCodes.Ldfld, srcField);
ilFunc.Emit(OpCodes.Ldloc_0);
ilFunc.Emit(OpCodes.Callvirt, methodInfoInvokeMember);
if (method.ReturnType.Equals(typeof(void)))
ilFunc.Emit(OpCodes.Pop);
else if (method.ReturnType.IsValueType)
ilFunc.Emit(OpCodes.Unbox_Any, method.ReturnType);
ilFunc.Emit(OpCodes.Ret);
}
The boxing of ValueTypes when passed as parameters or returned from the method is required, just like the property accessors.
The only real point of interest here is the array generation for the parameters - this also took me a little while to wrap my head around! You may note that I've been a bit lazy and not supported optional, out or ref parameters - I didn't need these for anything I was working on and didn't feel like diving into it at this point. I'm fairly sure that if they become important features then whipping out ildasm and looking at the generated code there will reveal the best way to proceed with these.
Now that we've defined everything about the class, we can instantiate it!
var wrapper = (ITest)Activator.CreateInstance(
typeBuilder.CreateType(),
src
);
This gives us back an instance of a magic new class that wraps a specified "src" and passes through the ITest properties and methods! If it's applied to an object that doesn't have the required properties and methods then exceptions will be thrown when they are called - the standard exceptions that reflection calls to invalid properties/methods would result in.
But I think that's pretty much enough for this installment - it's been a bit dry but I think it's been worthwhile!
There are a lot of extension points that naturally arise from this rough-from-the-edges code - the first things that spring to my mind are a way to wrap this up nicely into a generic class that could create wrappers for any given interface, a way to handle interface inheritance, a way to possibly wrap the returned values - eg. if we have
public interface ITest
{
IEmployee Get(int id);
}
public interface IEmployee
{
int Id { get; }
string Name { get; }
}
can the method that applies ITest to an object also apply IEmployee to the value returned from ITest.Get if that value itself doesn't already implement IEmployee??
Finally, off the top of my head, if I'm using these generated classes to read interact with WSCs / COM components, am I going to need to pass references over to COM components? If so, I'm going to have to find a way to flag them as ComVisible.
But these are issues to address another time :)
The code here doesn't use the .Net 4.0 "dynamic" keyword and so will compile under .Net 2.0. I had a bit of a poke around in the IL generated that makes use of dynamic since in some situations it should offer benefits - often performance is one such benefit! However, in the particularly niche scenario I'm working with it refuses to work :( Most of the components I'm wrapping are legacy VBScript WSCs and trying to set properties on these seems to fail when using dynamic.
I've pulled this example from a test (in xUnit) I wrote to illustrate that there were issues ..
[Fact]
public void SettingCachePropertyThrowsRuntimeBinderException()
{
// The only way to demonstrate properly is unfortunately by loading an actual wsc - if we used a
// standard .Net class as the source then it would work fine
var src = Microsoft.VisualBasic.Interaction.GetObject(
Path.Combine(
new FileInfo(this.GetType().Assembly.FullName).DirectoryName,
"TestSrc.wsc"
)
);
var srcWithInterface = new ControlInterfaceApplierUsingDynamic(src);
// Expect a RuntimeBinderException with message "'System.__ComObject' does not contain a definition
// for 'Cache'"
Assert.Throws<RuntimeBinderException>(() =>
{
srcWithInterface.Cache = new NullCache();
});
}
public class ControlInterfaceApplierUsingDynamic : IControl
{
private dynamic _src;
public ControlInterfaceApplierUsingDynamic(object src)
{
if (src == null)
throw new ArgumentNullException("src");
_src = src;
}
public ICache Cache
{
set
{
_src.Cache = value;
}
}
}
[ComVisible(true)]
private class NullCache : ICache
{
public object this[string key] { get { return null; } }
public void Add(string key, object value, int cacheDurationInSeconds) { }
public bool Exists(string key) { return false; }
public void Remove(string key) { }
}
The WSC content is as follows:
<?xml version="1.0" ?>
<?component error="false" debug="false" ?>
<package>
<component id="TestSrc">
<registration progid="TestSrc" description="Test Control" version="1" />
<public>
<property name="Config" />
</public>
<script language="VBScript">
<![CDATA[
Public Cache
]]>
</script>
</component>
</package>
I've not been able to get to the bottom of why this fails and it's not exactly a common problem - most people left WSCs back in the depths of time.. along with VBScript! But for the meantime we're stuck with them since the thought of trying to migrate the entire codebase fills me with dread, at least splitting it this way means we can move over piecemeal and re-write isolated components of the code at a time. And then one day the old cruft will have gone! And people will consider the earlier migration code the new "old cruft" and the great cycle can continue!
Update (2nd May 2014): It turns out that if
var src = Microsoft.VisualBasic.Interaction.GetObject(
Path.Combine(
new FileInfo(this.GetType().Assembly.FullName).DirectoryName,
"TestSrc.wsc"
)
);
is altered to read
var src = Microsoft.VisualBasic.Interaction.GetObject(
"script:"
Path.Combine(
new FileInfo(this.GetType().Assembly.FullName).DirectoryName,
"TestSrc.wsc"
)
);
then this test will pass! I'll leave the content here for posterity but it struck me while flipping through this old post that I had seen and addressed this problem since writing this.
Posted at 22:01
9 December 2011
"The under 25 crowd use a cell phone as their primary mode of accessing the Internet" - http://www.tnrglobal.com/blog.
Is this is a fact? I'm genuinely curious what this is based on. At 29 am I even older than I realised??
Posted at 00:25
8 December 2011
Continuing on with the proof-of-concept I'm doing at work regarding reimplementing a VBScript Engine with WSC Controls in .Net I've been trying to develop an ASP.Net MVC Controller that will execute in the STA ApartmentState having read this article:
http://msdn.microsoft.com/en-us/magazine/cc163544.aspx.
The upshot is that if components that run in STA are shared by something executing as MTA then only a single thread from the MTA worker can operate on the component at a time. If the caller is running as STA then separate instances will exist such that each request (I'm thinking in terms of ASP.Net MVC requests) gets its own instance, preventing requests getting queued up waiting for each other when accessing the STA components.
ASP.Net WebForms Pages support an "ASPCompat" attribute which will create the request as STA, rather than MTA. The article I linked above demonstrates how to do similar for an asmx web service. And the forum answer here claims to describe how to do the same for ASP.Net MVC: http://forums.asp.net/t/1302406.aspx.
However..
I'm not sure what version of MVC that was for, and if things have changed since then (it's marked August 2008), but when I tried to use it it didn't compile :(
So here's the version I'm using with the MVC 3 / .Net 4.0 project I've got on the go - we need an IRouteHandler implementation which makes use of an STA-inducing Handler. Thus:
public class STAThreadRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
if (requestContext == null)
throw new ArgumentNullException("requestContext");
return new STAThreadHttpAsyncHandler(requestContext);
}
}
public class STAThreadHttpAsyncHandler : Page, IHttpAsyncHandler, IRequiresSessionState
{
private RequestContext _requestContext;
public STAThreadHttpAsyncHandler(RequestContext requestContext)
{
if (requestContext == null)
throw new ArgumentNullException("requestContext");
_requestContext = requestContext;
}
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
{
return this.AspCompatBeginProcessRequest(context, cb, extraData);
}
protected override void OnInit(EventArgs e)
{
var controllerName = _requestContext.RouteData.GetRequiredString("controller");
var controllerFactory = ControllerBuilder.Current.GetControllerFactory();
var controller = controllerFactory.CreateController(_requestContext, controllerName);
if (controller == null)
throw new InvalidOperationException("Could not find controller: " + controllerName);
try
{
controller.Execute(_requestContext);
}
finally
{
controllerFactory.ReleaseController(controller);
}
this.Context.ApplicationInstance.CompleteRequest();
}
public void EndProcessRequest(IAsyncResult result)
{
this.AspCompatEndProcessRequest(result);
}
public override void ProcessRequest(HttpContext httpContext)
{
throw new NotSupportedException(
"STAThreadRouteHandler does not support ProcessRequest called (only BeginProcessRequest)"
);
}
}
Then in the routes defined in Global.asx.cs we need something along the lines of:
RouteTable.Routes.Add(new Route(
"{*url}",
new RouteValueDictionary(new { controller = "Default", action = "PageRequest" }),
new STAThreadRouteHandler()
));
in place of
routes.MapRoute(
"Default",
"{*url}",
new { controller = "Default", action = "PageRequest" }
);
This post has been quite derivative of other works but it took me a fair amount of researching to get to this point! Maybe this will benefit someone else going down a similar windy path..
###IRequiresSessionState
Of particular note (and absent from the referenced forum answer) is the IRequiresSessionState implemented by STAThreadRouteHandler. This interface has no methods or properties but identifies the Handler as being one that requires that Session State be passed to it.. er, as the name implies! But without this, the Session property of the specified Controller will always be null. This took me quite a while to track down since - unless you know of this particular interface - it's fairly difficult information to track down! Or maybe I was just having a bad Google day.. :)
Posted at 23:46
8 December 2011
At work today, in a fit of madness, I decided to rename a file in a BitBucket (ie. Mercurial) repository from RSSController.cs to RssController.cs for consistency with the casing in other files. At the time I wondered if this rename was a good idea, but everything seemed to go well.
When I got home and tried to update my repository there, seemed became the operative word. I was greeted with a "case folding collision" error and the start of 90 minutes of my life that feel wasted and aren't ever going to come back.
I'm not sure if there's a good way to do this and a lot of information out there about resolving this sort of mess starts with "On the Linux/FreeBSD machine.." or "On a case sensitive OS.." which is not very useful when all my computers run Windows!
Long story short; this page helped me out a lot - http://mercurial.selenic.com/wiki/FixingCaseCollisions. I haven't tried the CaseFoldExtension but following the instructions in there sorted me out. The only issue I encountered was one of the commands complained that it didn't have a username, including an additional -u"user@wherever.com" sorted that out.
This has tired me out! :S
Posted at 20:50
6 December 2011
Recently I've been using IIS Express, mostly at home where I don't have a fancy OS with a full IIS installation. Integrated with Visual Studio it's great at being an environment closer-to-real-IIS than the built-in VS WebServer / Casini (is it even still called that these days??) but recently I've been doing some proof-of-concept work for an Engine that integrates with some Legacy WSC components we have in a project at work. Oh yes, the bleeding edge of VBScript in pseudo-COM wrappers - awesome :S And for this work there are stretches of time where I don't want the overhead of the debugger being attached and be able to flip the app pool and re-load the components and using WebMatrix to configure the site has allowed me to do just that.
But even with WebMatrix there isn't a huge amount of configuring that can be done through the GUI. I'd heard that IIS Express could support Virtual Directories and Virtual Applications but I just couldn't find where to do it through the interface! And I still can't.. but the good news is that the config files for IIS Express - like IIS 7 - are really easy to get into!
Since IIS Express installs against the user account (which contributes to its non-requirement of Admin rights), the config files are located in the "IISExpress\config" folder under "My Documents". The interesting file here is "applicationhost.config" which describe all the sites under "configuration/system.applicationHost/sites". The default site may described as -
<site name="WebSite1" id="1" serverAutoStart="true">
<application path="/">
<virtualDirectory path="/" physicalPath="%IIS_SITES_HOME%\WebSite1" />
</application>
<bindings>
<binding protocol="http" bindingInformation=":8080:localhost" />
</bindings>
</site>
to add a Virtual Directory is as easy as adding a new virtualDirectory node such as -
<site name="WebSite1" id="1" serverAutoStart="true">
<application path="/">
<virtualDirectory path="/" physicalPath="%IIS_SITES_HOME%\WebSite1" />
<virtualDirectory
path="/config"
physicalPath="C:\Documents and Settings\Dan\My Documents\Projects\Config"
/>
</application>
<bindings>
<binding protocol="http" bindingInformation=":8080:localhost" />
</bindings>
</site>
while adding a new Virtual Directory is done with a new application node -
<site name="WebSite1" id="1" serverAutoStart="true">
<application path="/">
<virtualDirectory path="/" physicalPath="%IIS_SITES_HOME%\WebSite1" />
</application>
<application path="/config">
<virtualDirectory
path="/"
physicalPath="C:\Documents and Settings\Dan\My Documents\Projects\Config"
/>
</application>
<bindings>
<binding protocol="http" bindingInformation=":8080:localhost" />
</bindings>
</site>
Well it's hardly rocket science! But it took me a while to get this sorted out.. there are quite a few questions out there to this effect but it seems like once someone works it out they think it's obvious and so there aren't that many answering posts out there.
This is my contribution to that end :)
Posted at 18:11
Dan is a big geek who likes making stuff with computers! He can be quite outspoken so clearly needs a blog :)
In the last few minutes he seems to have taken to referring to himself in the third person. He's quite enjoying it.