Showing posts with label hackery. Show all posts
Showing posts with label hackery. Show all posts

20110504

Adding a WCF function to a service, in code, at runtime

Working on something last week I thought it might be a good idea to have an attribute that added an extra endpoint to a service at runtime.  I coded for a bit making decent progress, got stuck, and when I started to google I saw several discussions of it not being possible, and one discussion about how to do it by modifying the host (http://zamd.net/2010/02/05/adding-dynamic-methods-to-a-wcf-service/).  It turns out that after the service is running some things seem to be locked down that you would need to pull this trick off.

Remembering some similar issues in HttpModules/HttpApplications I started looking for a point to do the injection of a method while the service is in the middle of loading and I found ServiceBehavior.AddBindingParameters.  The other two methods don't seem to be at the right point in the sequence for the injection to work correctly.

WARNING: lots of trial & error based code ahead, I doubt this is correct in all situations, it does work on a REST POX service fairly well.  I am mostly writing this so I remember it, and so others can see it probably can be done.

First you have to make an attribute that you will use later to decorate your service implementation.  In this example the AddUI sub looks at the first operation for the current contract, copies a lot of it's general settings then adds the messages to an operation, then the operation to the contract.

Imports System.ServiceModel.Description
 
Public Class ExternalInvalidatorAttribute
    Inherits Attribute
    Implements IServiceBehavior
 
    Public Sub AddBindingParameters(ByVal serviceDescription As System.ServiceModel.Description.ServiceDescription, ByVal serviceHostBase As System.ServiceModel.ServiceHostBase, ByVal endpoints As System.Collections.ObjectModel.Collection(Of System.ServiceModel.Description.ServiceEndpoint), ByVal bindingParameters As System.ServiceModel.Channels.BindingParameterCollection) Implements System.ServiceModel.Description.IServiceBehavior.AddBindingParameters
        For Each endpt In endpoints
            AddUI(endpt)
        Next
    End Sub
 
    Private Sub AddUI(ByRef endpt As ServiceEndpoint)
        Dim cd = endpt.Contract.Operations(0).DeclaringContract
        Dim od = New OperationDescription("invalidatorUI", cd)
 
        Dim inputMsg = New MessageDescription(cd.Namespace + cd.Name + "/invalidatorUI", MessageDirection.Input)
        Dim mpd = New MessagePartDescription("a", endpt.Contract.Namespace)
        mpd.Index = 0
        mpd.MemberInfo = Nothing
        mpd.Multiple = False
        mpd.ProtectionLevel = Net.Security.ProtectionLevel.None
        mpd.Type = GetType(System.String)
        inputMsg.Body.Parts.Add(mpd)
        od.Messages.Add(inputMsg)
 
        Dim outputMsg = New MessageDescription(cd.Namespace + cd.Name + "/invalidatorUIResponse", MessageDirection.Output)
        outputMsg.Body.ReturnValue = New MessagePartDescription("invalidatorUIResult", cd.Namespace) With {.Type = GetType(System.String)}
        od.Messages.Add(outputMsg)
 
        od.Behaviors.Add(New DataContractSerializerOperationBehavior(od))
        od.Behaviors.Add(New System.ServiceModel.Web.WebGetAttribute() With {.UriTemplate = "/invalidator/{a}"})
        od.Behaviors.Add(New System.ServiceModel.OperationBehaviorAttribute())
        Dim dc = New DasOP()
 
        od.Behaviors.Add(dc)
 
        cd.Operations.Add(od)
    End Sub
 
 
#Region "not needed"
 
    Public Sub ApplyDispatchBehavior(ByVal serviceDescription As System.ServiceModel.Description.ServiceDescription, ByVal serviceHostBase As System.ServiceModel.ServiceHostBase) Implements System.ServiceModel.Description.IServiceBehavior.ApplyDispatchBehavior
    End Sub
 
    Public Sub Validate(ByVal serviceDescription As System.ServiceModel.Description.ServiceDescription, ByVal serviceHostBase As System.ServiceModel.ServiceHostBase) Implements System.ServiceModel.Description.IServiceBehavior.Validate
    End Sub
 
#End Region
 
End Class

Since there is no function in the service implementation the default invoker call would error with nothing to act upon, so the DasOP custom operation behavior is substituted for the default.  Looking at it's apply dispatch sub below you can see it is just a crutch to a custom invoker.  You don't have to do this, but if you don't you have to deal with the message objects by hand, and they are not really that friendly. 

Imports System.ServiceModel.Description
 
Public Class DasOp
    Inherits Attribute                                  'this makes it a decorator
    Implements IOperationBehavior                       'this makes it get called for applybehaviour
 
    Public Sub ApplyDispatchBehavior(ByVal operationDescription As System.ServiceModel.Description.OperationDescription, ByVal dispatchOperation As System.ServiceModel.Dispatcher.DispatchOperation) Implements System.ServiceModel.Description.IOperationBehavior.ApplyDispatchBehavior
        dispatchOperation.Invoker = New InvalidatorInvoker()    'this invoker actually does the work, it needs a reference to the other cache objects so it can meddle in their cache arrays
    End Sub
 
#Region "not used"
    Public Sub AddBindingParameters(ByVal operationDescription As System.ServiceModel.Description.OperationDescription, ByVal bindingParameters As System.ServiceModel.Channels.BindingParameterCollection) Implements System.ServiceModel.Description.IOperationBehavior.AddBindingParameters
        'not needed
    End Sub
 
    Public Sub ApplyClientBehavior(ByVal operationDescription As System.ServiceModel.Description.OperationDescription, ByVal clientOperation As System.ServiceModel.Dispatcher.ClientOperation) Implements System.ServiceModel.Description.IOperationBehavior.ApplyClientBehavior
        'not needed
    End Sub
 
    Public Sub Validate(ByVal operationDescription As System.ServiceModel.Description.OperationDescription) Implements System.ServiceModel.Description.IOperationBehavior.Validate
        'not needed
    End Sub
#End Region
 
End Class

Finally you make the invoker, it doesn't really invoke anything, since nothing actually exists to invoke, but it allocates and input and returns a result like their was, so the rest of the WCF seems not to notice the difference.

Imports System.ServiceModel.Dispatcher
Imports System.Xml.Linq
Imports System.Text.RegularExpressions
 
Public Class InvalidatorInvoker
    Implements IOperationInvoker
 
    Public Function AllocateInputs() As Object() Implements System.ServiceModel.Dispatcher.IOperationInvoker.AllocateInputs
        Return {Nothing}    'reserve a spot for some input
    End Function
 

   Public Function Invoke(ByVal instance As Object, ByVal inputs() As Object, ByRef outputs() As Object) As Object Implements System.ServiceModel.Dispatcher.IOperationInvoker.Invoke
        outputs = New Object(-1) {}     'return an empty array here, MSDN does not elaborate as to why
        Return "Result"
   End Function
 
#Region "not needed"
 
    Public Function InvokeBegin(ByVal instance As Object, ByVal inputs() As Object, ByVal callback As System.AsyncCallback, ByVal state As Object) As System.IAsyncResult Implements System.ServiceModel.Dispatcher.IOperationInvoker.InvokeBegin
        Return Nothing
    End Function
 
    Public Function InvokeEnd(ByVal instance As Object, ByRef outputs() As Object, ByVal result As System.IAsyncResult) As Object Implements System.ServiceModel.Dispatcher.IOperationInvoker.InvokeEnd
        Return Nothing
    End Function
 
#End Region
 
    Public ReadOnly Property IsSynchronous As Boolean Implements System.ServiceModel.Dispatcher.IOperationInvoker.IsSynchronous
        Get
            Return True     'disable async
        End Get
    End Property
 
End Class

It isn't pretty, but it works.

20110328

Microsoft.Web.Infrastructure + System.Web.PreApplicationStartMethod

What follows is interesting & a neat trick, but I do not claim that it is a good idea to do this in production code.  It may be useful, it may not.

I have been playing around with a few of the new bits that are available for .NET web development recently and I came across a neat trick.  I had a project that I wanted to debug, but I didn't have access to the source directly and the app itself was handling the error and in the process eating some of the detail I needed.

I made a httpmodule that was decorated with System.Web.PreApplicationStartMethod This is a framework 4 bit that allows a httpmodule to run a shared sub BEFORE all the stack gets built and locked in place.

I then took and used reflection to load and invoke Microsoft.Web.Infrastructure.DynamicModuleHelper.DynamicModuleUtility.RegisterModule.  This allows the httpmodule to place itself in the modules collection without altering the web config.  Loading it through reflection allows me to just drag the dll into the bin folder along with the httpmodule dll.  This is handy because web.infrastructure is part of the MVC framework and the problem I was having was on a non-mvc machine.

A little poking about in reflector and I came up with this function to get the existing event handlers



Dim t As Type = target.[GetType]()
Public Function GetEventSubscribers(ByVal target As Object, ByVal eventName As String) As [Delegate]()
    Dim w = CType(t.GetField("_events", BindingFlags.Instance Or BindingFlags.Static Or BindingFlags.NonPublic).
        GetValue(target), System.ComponentModel.EventHandlerList)
    Dim k = t.GetFields(BindingFlags.[Static] Or BindingFlags.Instance Or BindingFlags.NonPublic).
        Where(Function(x) x.Name.StartsWith("Event" & eventName)).Select(Function(x) x.GetValue(target)).ToList()
    Dim d() As [Delegate] = k.SelectMany(Function(x)
                                             If w(x) Is Nothing Then
                                                 Return New [Delegate]() {}
                                             Else
                                                 Return w(x).GetInvocationList()
                                             End If
                                         End Function).ToArray
    Return d
End Function

If you pass the httpapplication instance to it with an eventname you get all the registered handler delegates, which will allow you to call .removeeventhandler() on each of them.  

If you do that to the error event, then add your own handler, then re-add the pre-existing delegates in the correct order then your handler fires first, before any of the other handlers has a chance to mangle the even state and the rest of the application seems to be none the wiser as long as you don't alter the even state yourself.

In my example I just put a trace in that wrote to a text file and after a few passes at the broken bits I got a nice clear picture of what was going on.

The approach seems to work in ASPX, MVC, WCF and Sharepoint and only involves copying a couple dlls to the bin directory in the way of altering the original project.

Again, I haven't used this too much yet, so your mileage may vary, but even if it doesn't always work I think it is an interesting approach to spy on a misbehaving build without changing too many variables.

Firefox Feedly RSS option

If you use Firefox with a RSS button and want the default RSS page to offer a Feedly option here is what you need to do: go to the about:c...