One idea is to create dynamic code with automatic implementations of interfaces - proxy types.
Things You Can Do But Probably Will Not.
First thing we can try is to implement automatic calls to OnPropertyChanged
in properties setters.
Let's start with a simple WPF application with a single window defined in XAML as follows:
<Window x:Class="TestApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TestApp"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Text="{Binding Model.IntValue}"></TextBlock>
</StackPanel>
<StackPanel Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Button Command="{Binding Increment}"
Content="Increment" Padding="10"></Button>
</StackPanel>
</Grid>
</Window>
View model for main window looks like this:
using System.Windows.Input;
namespace TestApp
{
public class MainWindowViewModel
{
private ICommand _increment;
public MainWindowViewModel()
{
Model = new MainModel();
}
public ICommand Increment => _increment ?? (_increment = new Command(OnIncrement));
public MainModel Model { get; set; }
private void OnIncrement()
{
Model.Increment();
}
}
}
As you can see, we have a single text block and single button. View model has a single integer value and command that runs method on model.
Model looks like this:
namespace TestApp
{
public class MainModel
{
public int IntValue { get; set; }
public void Increment()
{
IntValue++;
}
}
}
Ok. After button click, text block value should be incremented.
How to do that? Binding should listen to OnPropertyChange
call of IPropertyChanged
interface. But for a lot of properties, it is quite a bit of work to implement all of them manually. Instead, we can do it automatically with proxy created via Reflection.Emit
.
public static class ProxyGenerator
{
public static T PropertyChangedProxy<T>() where T : class, new()
{
var type = typeof(T);
var assemblyName = type.FullName + "_Proxy";
var fileName = assemblyName + ".dll";
var name = new AssemblyName(assemblyName);
var assembly = AssemblyBuilder.DefineDynamicAssembly(name, AssemblyBuilderAccess.RunAndSave);
var module = assembly.DefineDynamicModule(assemblyName, fileName);
var typeBuilder = module.DefineType(type.Name + "Proxy",
TypeAttributes.Class | TypeAttributes.Public, type);
typeBuilder.DefineDefaultConstructor(MethodAttributes.Public);
var onPropertyChangedMethod = type.GetMethod("OnPropertyChanged",
BindingFlags.Instance | BindingFlags.NonPublic);
var propertyInfos = type.GetProperties().Where(p => p.CanRead && p.CanWrite);
foreach (var item in propertyInfos)
{
var baseMethod = item.GetGetMethod();
var getAccessor = typeBuilder.DefineMethod
(baseMethod.Name, baseMethod.Attributes, item.PropertyType, null);
var il = getAccessor.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.EmitCall(OpCodes.Call, baseMethod, null);
il.Emit(OpCodes.Ret);
typeBuilder.DefineMethodOverride(getAccessor, baseMethod);
baseMethod = item.GetSetMethod();
var setAccessor = typeBuilder.DefineMethod
(baseMethod.Name, baseMethod.Attributes, typeof(void), new[] { item.PropertyType });
il = setAccessor.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Call, baseMethod);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldstr, item.Name);
il.Emit(OpCodes.Call, onPropertyChangedMethod);
il.Emit(OpCodes.Ret);
typeBuilder.DefineMethodOverride(setAccessor, baseMethod);
}
var t = typeBuilder.CreateType();
assembly.Save(fileName);
return Activator.CreateInstance(t) as T;
}
}
Simple overwrite all properties of type and because of that, it works without problems.
The disadvantage of this solution is, even if it can be easily binded in XAML, it can't be called easily from code. Those properties are not visible via base type since this type does not have virtual properties. Considering that type with overridden properties is dynamically created, it cannot be called from code (because it does not exist in compile time). Only way to call them from code is via Reflection mechanism.
It makes it much less elegant.
Things You Can Do But Probably Should Not
The disadvantage of Reflection.Emit
is really poor documentation of how it is supposed to work and how it should be used. For example, I could not find a good example of how to define an event (I am talking about official Microsoft documentation). There is nothing about using TypeBuilder.DefineEvent
method on MSDN. Good thing there is StackOverflow and a lot of blogs like this one. Chances are really high that someone tried the same thing before.
Ok, going back to the subject. Previous implementation of proxy generation lacks automatic implementation of interface INotifyPropertyChanged
. You have to do it yourself in every class you want to create proxy of (or use some kind of base class, but we are talking about a problem when this is impossible or not recommended).
Good thing it is possible to implement this interface dynamically in proxy using Reflection.Emit
too. To do that, we need to create event PropertyChanged
, which requires to:
- Declare event field
- Add event declaration
- Add Add accessor
- Add Remove accessor
- Define raise method
Quite a few things to do, since in C# you have to do 2 things to create event:
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Define an event and raise method. Simple. In IL, things as you see above is are much more complicated.
Ok. To implement interface
dynamically, we have to first add this interface
to class
. In Reflection.Emit
, this requires only a single line, luckily.
typeBuilder.AddInterfaceImplementation(typeof(INotifyPropertyChanged));
After that, we can define event itself.
var field = typeBuilder.DefineField("PropertyChanged",
typeof(PropertyChangedEventHandler), FieldAttributes.Private);
var eventInfo = typeBuilder.DefineEvent("PropertyChanged",
EventAttributes.None, typeof(PropertyChangedEventHandler));
As you can see, we are defining field to store delegates and event itself. Both members are taken care of in C# by event definition. Also, the same definition in C# creates two accessors: add and remove. We can of course create accessors ourselves but it is very rarely needed. In IL, we have to do it manually. How? First, we need to explain few things.
Events in C# are handled by Delegate
types. Delegate
class has static
method Combine
, which combines two delegates into invocation list. And this mechanism is used to store all handlers in a single value of an event field. When event is raised, all methods from invocation list are called. To be able to use Delegate.Combine
, we have to retrieve this method via Reflection.
var combine = typeof(Delegate).GetMethod("Combine", new[] { typeof(Delegate), typeof(Delegate) });
Now, we can create add accessor method and implement it.
var ibaseMethod = typeof(INotifyPropertyChanged).GetMethod("add_PropertyChanged");
var addMethod = typeBuilder.DefineMethod("add_PropertyChanged",
ibaseMethod.Attributes ^ MethodAttributes.Abstract,
ibaseMethod.CallingConvention,
ibaseMethod.ReturnType,
new[] { typeof(PropertyChangedEventHandler) });
var generator = addMethod.GetILGenerator();
var combine = typeof(Delegate).GetMethod("Combine", new[] { typeof(Delegate), typeof(Delegate) });
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldfld, field);
generator.Emit(OpCodes.Ldarg_1);
generator.Emit(OpCodes.Call, combine);
generator.Emit(OpCodes.Castclass, typeof(PropertyChangedEventHandler));
generator.Emit(OpCodes.Stfld, field);
generator.Emit(OpCodes.Ret);
eventInfo.SetAddOnMethod(addMethod);
Few things need clarification. First of all, we are implementing interface
method (even if interface
does not have this method declared explicitly) and we need this method metadata for implementation to work. After we get interface
method info, we can use it to create override. Overridden methods have to have the same parameters, return type, calling conventions and attributes (without Abstract
, because it is method with implementation).
There are only few IL codes in implementation, but it needs explanation anyway.
First, IL is not C# so you need to look at it a little differently. In C# method, you have state in form of set of temporary variables (if it is a pure method) and in class itself. In IL, you have only stack which is kind of a state of a IL method. Variables on stack determine source of fields, instance methods, and methods parameters. In the above example, what was most difficult to understand for me were the first two lines. Why there is a loading of first method argument two times to stack? Let's look at the picture below. First 'row' is a stack after execution of first two codes: Ldarg_0
.
We load source instance (C# this
) two times which is always the first argument of a instance method (even if this is not visible in C#). After that, we have two items on stack, both are reference to the same object. After next operation (Ldfld
) which takes only one argument (assembly operations usually take none or one argument), we also have two values on stack (source instance and value of field, which replaces second reference to source instance). Next operations (Ldarg_1
) loads second argument of method on top a stack which causes stack to contains three values. Top two items from stack are passed to Delegate.Combine
call, which replaces two items on top of stack with new combined delegate. Next IL code (Castclass
) replaces item on top by casting it to appropriate type of event, which is then used to set field of source instance with new value of event handler (Stfld
). Returned value is void
so the rest of the stack is discarded and none value is returned from method (if method is non void, top item from stack is returned).
With added implementation of method add_PropertyChanged
, most of this code can be reused for remove accessor, but instead of Delegate.Combine
, we use Delegate.Remove
.
var ibaseMethod = typeof(INotifyPropertyChanged).GetMethod("remove_PropertyChanged");
var removeMethod = typeBuilder.DefineMethod("remove_PropertyChanged",
ibaseMethod.Attributes ^ MethodAttributes.Abstract,
ibaseMethod.CallingConvention,
ibaseMethod.ReturnType,
new[] { typeof(PropertyChangedEventHandler) });
var remove = typeof(Delegate).GetMethod("Remove", new[] { typeof(Delegate), typeof(Delegate) });
var generator = removeMethod.GetILGenerator();
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldfld, field);
generator.Emit(OpCodes.Ldarg_1);
generator.Emit(OpCodes.Call, remove);
generator.Emit(OpCodes.Castclass, typeof(PropertyChangedEventHandler));
generator.Emit(OpCodes.Stfld, field);
generator.Emit(OpCodes.Ret);
eventInfo.SetRemoveOnMethod(removeMethod);
Much more interesting is implementation of raise
method. C# implementation is pretty straightforward.
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
We check if event has any handlers and invoke all of them. In IL, things are much more complicated.
var methodBuilder = typeBuilder.DefineMethod("OnPropertyChanged",
MethodAttributes.Family | MethodAttributes.Virtual | MethodAttributes.HideBySig |
MethodAttributes.NewSlot, typeof(void),
new[] { typeof(string) });
var generator = methodBuilder.GetILGenerator();
var returnLabel = generator.DefineLabel();
var propertyArgsCtor = typeof(PropertyChangedEventArgs).GetConstructor(new[] { typeof(string) });
generator.DeclareLocal(typeof(PropertyChangedEventHandler));
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldfld, field);
generator.Emit(OpCodes.Stloc_0);
generator.Emit(OpCodes.Ldloc_0);
generator.Emit(OpCodes.Brfalse, returnLabel);
generator.Emit(OpCodes.Ldloc_0);
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldarg_1);
generator.Emit(OpCodes.Newobj, propertyArgsCtor);
generator.Emit(OpCodes.Callvirt, typeof(PropertyChangedEventHandler).GetMethod("Invoke"));
generator.MarkLabel(returnLabel);
generator.Emit(OpCodes.Ret);
eventInfo.SetRaiseMethod(methodBuilder);
return methodBuilder;
Definition of this method (attributes) is taken from ILDASM application, used to inspect IL code generated from above C# code. I did not experiment with it nor I did I dwell on why it is written like that. After all, it is not a point and specialist and Microsoft knows what they are doing (at least I hope so ). My intention was to have IL code to work as much as C# event as it is possible.
New things among IL operation codes is label declaration. Thing is: in assemblers language, you do not have blocks, closures, functions, etc. You only have stream of operations and jumps for flow control. So instead of if
statement, there is a label for return from method - if value of event handlers are null
(no handlers for event), program jumps to end of method. If there are handlers, they are invoked and method ends.
Let's go through operations step by step.
First, of course, we load instance of object (which is the first argument with index of 0
). After that, we load field from that instance represented by PropertyChanged
field of event. After loading on to a stack, we store it in method variable (which is why DeclareLocal
is called earlier). It is because we need this value twice: first to check if it is not null
and second for calling it. Brfalse
operation checks if value is boolean false
. In C#, we have to check if value of a class is null
or not null
explicitly. In IL assembly language, the same operation checks if value is boolean false
, integer 0
or null
. If it is 'false
', program control jumps to return label. If not, it continues and loads value of event again from local variable (first one was discarded by Brfalse
operation). Next, we load instance of proxy MainModelProxy
and name of changed property (in second parameter of method, at 1 index - Ldarg_1
). Newobj
operation calls given constructor. Since this constructor takes one parameter (name of property), this stack value is replaced by new object. So we have two items in stack: instance of MainModelProxy
class and new instance of PropertyChangedEventArgs
. With those values available, we can safely invoke Invoke
method of PropertyChangedEventHandler
(which is kind of static
method since it does not take first argument as instance). This method does not return value so we can return from dynamic OnPropertyChanged
method with Ret
operation.
Ok. After removing implementation from MainModel
class and running this code, we will see the same results as before: label text will show next value of integer after each button press, which proves that IL implementation of event works fine. Why is there is a problem then? After all, this section is titled things you 'should not do', right? Let's jump back to C# implementation in MainModel
class and inspect details with ILDASM.exe. We will see the following IL code in add_PropertyChanged
method.
.method public hidebysig newslot specialname virtual final
instance void add_PropertyChanged
(class [System]System.ComponentModel.PropertyChangedEventHandler 'value') cil managed
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() =
( 01 00 00 00 )
.maxstack 3
.locals init (class [System]System.ComponentModel.PropertyChangedEventHandler V_0,
class [System]System.ComponentModel.PropertyChangedEventHandler V_1,
class [System]System.ComponentModel.PropertyChangedEventHandler V_2)
IL_0000: ldarg.0
IL_0001: ldfld class [System]System.ComponentModel.PropertyChangedEventHandler
TestApp.MainModel::PropertyChanged
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: stloc.1
IL_0009: ldloc.1
IL_000a: ldarg.1
IL_000b: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine
(class [mscorlib]System.Delegate,
class [mscorlib]System.Delegate)
IL_0010: castclass [System]System.ComponentModel.PropertyChangedEventHandler
IL_0015: stloc.2
IL_0016: ldarg.0
IL_0017: ldflda class [System]System.ComponentModel.PropertyChangedEventHandler
TestApp.MainModel::PropertyChanged
IL_001c: ldloc.2
IL_001d: ldloc.1
IL_001e: call !!0 [mscorlib]System.Threading.Interlocked::CompareExchange(!!0&,
!!0,
!!0)
IL_0023: stloc.0
IL_0024: ldloc.0
IL_0025: ldloc.1
IL_0026: bne.un.s IL_0007
IL_0028: ret
}
As you can see, it is entirely different. There is a loop that checks for a value returned from System.Threading.Interlocked.CompareExchange
method! What for? Probably because of thread safe code for changing of event handlers invocation list. And this is out of a box with new C# compiler. Since there really is no point in informing a C# users of every change of how C# compiler compiles code to IL, if you are interested, you have to inspect it yourself. It is a lot of work to keep your IL code in line with what Microsoft does. So even if you can write IL code that is supposed to do the same as IL code generated by C# compiler - you can't be sure unless you check it yourself. Add to that, a very troublesome process of creating such IL code (you can't debug it; you can't really tell what is wrong with it - you just will be greeted with ubiquitous 'IL code is not correct' error without details) - I strongly urge you to write IL code with Reflection.Emit
that only does:
- Calls to other C# methods
- Casts objects to other types
Any other, more complicated code can be written in C# and then invoked via IL. It really does not matter if method is instance one (unless it uses state of object) or static
one. For example, in my opinion, it is better to have a method that takes care of null
-checking, creating instance of PropertyChangedEventArgs
and calling PropertyChangeHandler
. Consider the following static
method.
public static class PropertyChangedInvoker
{
public static void Invoke(INotifyPropertyChanged sender,
PropertyChangedEventHandler source, string propertyName)
{
source?.Invoke(sender, new PropertyChangedEventArgs(propertyName));
}
}
This static
method in static
class can be safely called from IL and in those all the logic that was previously implemented in IL inside OnPropertyChanged
method! Thing is we have really easy to read and (most important!) debuggable, testable code. In IL, it is not possible. The only thing left to do in IL is to call this method with correct set of parameters.
var methodBuilder = typeBuilder.DefineMethod("OnPropertyChanged",
MethodAttributes.Family | MethodAttributes.Virtual | MethodAttributes.HideBySig |
MethodAttributes.NewSlot, CallingConventions.Standard | CallingConventions.HasThis, typeof(void),
new[] { typeof(string) })
var generator = methodBuilder.GetILGenerator()
var returnLabel = generator.DefineLabel()
generator.DeclareLocal(typeof(PropertyChangedEventHandler))
generator.Emit(OpCodes.Ldarg_0)
generator.Emit(OpCodes.Ldarg_0)
generator.Emit(OpCodes.Ldfld, field)
generator.Emit(OpCodes.Ldarg_1)
generator.Emit(OpCodes.Call, typeof(PropertyChangedInvoker).GetMethod("Invoke"))
generator.MarkLabel(returnLabel)
generator.Emit(OpCodes.Ret)
eventInfo.SetRaiseMethod(methodBuilder)
return methodBuilder
As you can see, in IL, we only collect set of parameters. Sender from this
in first argument of a instance method OnPropertyChanged
, event field value and property name for event arguments constructor.
On a side note, IMHO, it is funny that we can't directly invoke event outside of a class unless we access event field value first.
Someone may say that this IL is not really that less complicated. However, if you will look closer, it is totally independent from changes of event definition, event arguments class definition and event delegate definition. If you will not change number of arguments needed by raise
method and arguments class parameters (of course, there is a really small chance that those things change in PropertyChangeEventHandler
since it is a .NET class) you are safe and sound.
Point from all of this is:
Use as little IL as possible, since working with it in VS is quite painful.
Things You Can Do and Probably Should Because It's Cool
I am currently working on mobile Xamarin applications. One of most missing features is WCF (and other services) proxies. It is possible to use third party libraries, but I could not find one that suits my needs 100%. Because of that I decided to write proxy generating mechanism myself. This is an ideal place for Reflection.Emit
since you have almost the same code for every HTTP/HTTPS call with small differences like HTTP method (GET
, POST
, etc.) or return/parameter type(s).
Consider the following self-hosted JSON WCF service defined as follows:
public class Service : IService
{
public int Add(AddRequest req)
{
return req.FirstNumber + req.SecondNumber;
}
public string Test(string param)
{
return param;
}
}
As you can see, we have two methods: Test
(simple echo) and Add
that sums two arguments. Nothing fancy, but we do not need anything more since it is enough to show use of Reflection.Emit
in this scenario.
Contract is defined in interface IService
.
[ServiceContract]
public interface IService
{
[OperationContract]
[WebInvoke(Method = "POST",
RequestFormat = WebMessageFormat.Json,
ResponseFormat = WebMessageFormat.Json)]
int Add(AddRequest req);
[OperationContract]
[WebGet]
string Test(string param);
}
First one uses POST HTTP method and transports messages using JSON. Second one: GET
and simple string
s.
Self hosting is done by simple code in the second test, console application.
static void Main(string[] args)
{
var serviceHost = new ServiceHost(typeof(Service), new Uri("http://localhost:8081"));
using (serviceHost)
{
var seb = new WebHttpBehavior
{
DefaultOutgoingRequestFormat = WebMessageFormat.Json,
DefaultOutgoingResponseFormat = WebMessageFormat.Json,
FaultExceptionEnabled = true
};
serviceHost.Description.Behaviors.Add(new ServiceMetadataBehavior { HttpGetEnabled = true });
var e = serviceHost.AddServiceEndpoint(typeof(IService), new WebHttpBinding(), "");
e.Behaviors.Add(seb);
serviceHost.Open();
Console.WriteLine("Service ready...");
Console.ReadLine();
serviceHost.Close();
}
}
Ok. This is really simple code. Almost minimal code to run self-hosted JSON service. How to call it from WPF application? HttpClient
.NET class is more than enough.
private void OnCallService()
{
using (var client = new HttpClient(new HttpClientHandler()))
{
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var addRequest = new AddRequest
{
FirstNumber = First,
SecondNumber = Second
};
var serializeObject = JsonConvert.SerializeObject(addRequest);
var call = client.PostAsync("http://localhost:8081/add",
new StringContent(serializeObject, Encoding.UTF8, "application/json"));
try
{
call.Wait();
var result = call.Result.Content;
Sum = (int)JsonConvert.DeserializeObject(result.ReadAsStringAsync().Result, typeof(int));
}
catch (System.Exception)
{
}
}
}
Simple using
statement with HttpClient
instance, configuring it for POST
request with JSON string
and deserializing result value from call, to appropriate type.
How can we automate this with Reflection.Emit
and proxy generation? First of all, we need some type as base for our proxy. Luckily, WCF services have contracts and they are usually shared with client anyway. We can implement this interface!
Ok, first of all, let's go back to ProxyGenerator
class. We can add new method ServiceProxy
to it for generating service proxies.
public static T ServiceProxy<T>(string baseUrl) where T : class
{
var serviceInterface = typeof(T);
if (serviceInterface.IsInterface)
{
var assemblyName = serviceInterface.FullName + "_Proxy";
var fileName = assemblyName + ".dll";
var name = new AssemblyName(assemblyName);
var assembly = AssemblyBuilder.DefineDynamicAssembly(name, AssemblyBuilderAccess.RunAndSave);
var module = assembly.DefineDynamicModule(assemblyName, fileName);
var implemntationName = serviceInterface.Name.StartsWith("I") ?
serviceInterface.Name.Substring(1) : serviceInterface.Name;
var typeBuilder = module.DefineType(implemntationName + "Proxy",
TypeAttributes.Class | TypeAttributes.Public);
typeBuilder.AddInterfaceImplementation(serviceInterface);
foreach (var method in serviceInterface.GetMethods().Where(m => !m.IsSpecialName))
{
var customAttributes = method.GetCustomAttributes<OperationContractAttribute>()
.SingleOrDefault();
if (customAttributes != null)
{
var webInvokeAttr = method.GetCustomAttribute<WebInvokeAttribute>();
var webGetAttr = method.GetCustomAttribute<WebGetAttribute>();
ImplementServiceMethod(baseUrl, typeBuilder, method, webInvokeAttr, webGetAttr);
}
else
{
throw new Exception("Service interface has to be marked with correct method attribute!");
}
}
var type = typeBuilder.CreateType();
assembly.Save(assemblyName);
return (T)Activator.CreateInstance(type);
}
return null;
}
First few lines is nothing new (already done in proxies for property changed event) nor is it interesting. Fun begins with adding service implementation to our new type. We of course need service interface in proxy type and that is why AddInterfaceImplementation
method is used. Next thing to do is to search for all interface methods (that are not properties accessors; since all get_{property}
and set_{property}
methods have IsSpecialName
set to true
). In all methods, we search for WeGet
or WebInvoke
attribute. Those attributes are required in service methods since without it, we do not know what kind of url or HTTP method should be used. Core implementation of service method call is done by ImplementServiceMethod
method.
private static void ImplementServiceMethod(string baseUrl, TypeBuilder typeBuilder, MethodInfo method,
WebInvokeAttribute webInvokeAttr, WebGetAttribute webGetAttr)
{
var parameterTypes = method.GetParameters().Select(p => p.ParameterType).ToArray();
var methodBuilder = typeBuilder.DefineMethod
(method.Name, method.Attributes ^ MethodAttributes.Abstract,
method.CallingConvention, method.ReturnType,
parameterTypes);
var il = methodBuilder.GetILGenerator();
var serviceCallMethod = typeof(ProxyGenerator).GetMethod("BaseServiceCall",
BindingFlags.Public | BindingFlags.Static).MakeGenericMethod
(parameterTypes[0], method.ReturnType);
var url = new Uri(new Uri(baseUrl), method.Name).AbsoluteUri;
if (webGetAttr != null)
{
url = url + "?" + method.GetParameters()[0].Name + "=";
}
il.Emit(OpCodes.Ldstr, url);
il.Emit(OpCodes.Ldstr, webGetAttr != null ? "GET" : "POST");
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Call, serviceCallMethod);
il.Emit(OpCodes.Ret);
}
Core of the logic is handled by adding IL codes. But first, we need to define method override using the same parameters, return type, method attributes and calling conventions as interface method (again attributes are the same except for Abstract
one, since this one marks only abstract
methods and interface
methods). Of course, for simplicity, we use only single parameter for implementation. If you are using DTO for services methods, they should only have one parameter (DTO class). Second, if you have more than one parameter, you certainly should use DTO class (it makes rewriting, refactoring much easier, which happens always, sooner or later); third if you have more than one custom DTO class method (there is really no point in doing that), you are probably doing something wrong.
Anyway, if your service method has more than one parameter, you should refactor it to use only one and if it is not possible, you can easily change collection of IL codes to load one (or more) extra parameter before calling BaseServiceCall
method.
Then we change url if service method should be called with GET
(because parameter should be serialized for query string). Of course, here is the place to implement url template (i.e., /customer/{id}
), but this is a simple example, so call ToString
on request object is enough to make this work. After that, we can actually create new method body. Since this is a proxy method and the entire work is done by BaseServiceCall
, we just load three parameters (method url, HTTP method string
, and parameter of service method) and then execute call to method defined as follows:
public static TReturn BaseServiceCall<TParam, TReturn>(string url, string method, TParam param)
{
using (var client = new HttpClient(new HttpClientHandler()))
{
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var serializeObject = JsonConvert.SerializeObject(param);
var stringContent = new StringContent(serializeObject, Encoding.UTF8, "application/json");
Task<HttpResponseMessage> call;
if (method == "POST")
{
call = client.PostAsync(url, stringContent);
}
else
{
call = client.GetAsync(url + param);
}
var result = call.Result.Content;
return (TReturn)JsonConvert.DeserializeObject
(result.ReadAsStringAsync().Result, typeof(TReturn));
}
}
With the above code, we can change our WPF application and test if this works. The below XAML should be added inside MainWindow.xaml file.
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Vertical"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Text="Service proxy test" FontWeight="Bold" />
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30" />
<RowDefinition Height="20" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Grid.Row="0" Text="First arg" />
<TextBlock Grid.Column="1" Grid.Row="0" Text="Second arg" />
<TextBox Grid.Column="0"
Grid.Row="1" x:Name="First"
Width="100" Text="{Binding First}" />
<TextBox Grid.Column="1"
Grid.Row="1"
x:Name="Second" Width="100" Text="{Binding Second}" />
</Grid>
<TextBlock x:Name="Sum" Text="{Binding Sum}" />
</StackPanel>
<StackPanel HorizontalAlignment="Center" Grid.Row="1"
Grid.Column="1"
VerticalAlignment="Center">
<Button Command="{Binding CallService}"
Content="Call Add" Padding="10" />
</StackPanel>
<Grid Grid.Row="2" Grid.Column="1"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0"
Text="Param:" />
<TextBox Grid.Row="0" Grid.Column="1"
Text="{Binding EchoParam}" />
<TextBlock Grid.Row="1" Grid.Column="0"
Text="Result:" />
<TextBlock Grid.Row="1" Grid.Column="1"
Text="{Binding Echo}" />
<Button Grid.Row="2" Grid.Column="0"
Grid.ColumnSpan="2" Content="Call Test"
Command="{Binding CallEcho}" />
</Grid>
Buttons binds to commands in view model handled by two functions:
private void OnCallEcho()
{
var serviceProxy = ProxyGenerator.ServiceProxy<IService>("http://localhost:8081");
Echo = serviceProxy.Test(EchoParam);
}
private void OnCallService2()
{
var serviceProxy = ProxyGenerator.ServiceProxy<IService>("http://localhost:8081");
Sum = serviceProxy.Add(new AddRequest
{
FirstNumber = First,
SecondNumber = Second,
});
}
Very simple code. First, create proxy with interface
and then call interface
method. It can't be any simpler.
After starting WPF application, we can try it out. After trying it out yourself, you should see similar results as in below GIF.
As you can see, it works without a problem.
Of course, there is no need for new proxy every time we want to call service and since proxy does not have any state, it can be used as singleton instance. Also, there is no need to create new type every time proxy is created. Assembly is saved as .dll file on disk and can be very easily loaded to domain and proxy type can be reused.
Reflection.Emit
is ideal for this scenario. We can create very easily proxy types from interface and simple manipulation of arguments, make base method do all heavy lifting. It is also really easy to maintain since you can change behavior of all methods in a single base method. For example, add logging, exception handling, alternative service server if first one will not respond, etc.
Example application can be downloaded from here or from Github.
If you want to try it out yourself, remember to start both projects (Service
and TestApp
) and run VS in administrator mode (it is required to start self-hosted WCF).