Can I use WcfFacility with WCF Test Client?

2020-07-30 03:21发布

问题:

I have a WCF Service Library containing a custom ServiceHostFactory derived from DefaultServiceHostFactory. I can't get the test client to use this factory. I just get the "no parameterless constructor was found" error.

Here's my hosting enviroment configuration:

<serviceHostingEnvironment>
  <serviceActivations>
    <add service="TestService.WcfLibrary.TestService"
         relativeAddress="/TestService.svc"
         factory="TestService.WcfLibrary.TestServiceHostFactory, TestService.WcfLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
  </serviceActivations>
</serviceHostingEnvironment>

Note that I don't have a .svc file actually. I am trying to use fileless activation.

Here's my custom ServiceHostFactory:

public class TestServiceHostFactory : DefaultServiceHostFactory
{
    public TestServiceHostFactory() : base(CreateKernel()) { }

    private static IKernel CreateKernel()
    {
        WindsorContainer container = new WindsorContainer();

        container.AddFacility<WcfFacility>();

        container.Register(Component.For<TestService>());
        container.Register(Component.For<IServiceManager>().ImplementedBy<ServiceManager>());

        return container.Kernel;
    }
}

It looks like this path is never executed. How can I get WCF Test Client to use my custom implementation?

回答1:

OK, this is possible, but it aint pretty...

First of all we need a way to hook in when the assembly is loaded since in this scenario their is no "main" or "application start" or anything. There is an interesting event on AppDomain called AssemblyLoaded that looks like it may do the trick, hmm, but how to hook into it, a way I thought to do this is to define a custom app domain manager so...

Create a brand new assembly and inside it define an interface, which can be implemented by some client and an AppDomainManager like so:

public interface IAssemblyLoaded
{
    void AssemblyLoaded();
}

public class CustomManager : AppDomainManager
{
    public override void InitializeNewDomain(AppDomainSetup appDomainInfo)
    {
        base.InitializeNewDomain(appDomainInfo);

        // Look for any classes that implement IAssemblyLoaded,
        // constuct them (will only work if they have a default constructor)
        // and call the AssemblyLoaded method
        var loaded = typeof (IAssemblyLoaded);
        AppDomain
            .CurrentDomain
            .AssemblyLoad += (s, a) =>
                                 {
                                     var handlers = a
                                         .LoadedAssembly
                                         .GetTypes()
                                         .Where(loaded.IsAssignableFrom);

                                     foreach (var h in handlers)
                                     {
                                         ((IAssemblyLoaded)
                                          Activator.CreateInstance(h))
                                             .AssemblyLoaded();
                                     }
                                 };
    }
}

Make sure the assembly is signed and then add it to the GAC. Let's say I called the assembly AssemblyInitCustomDomainManager I can add it like so (and I get back the details about it straight away because I will need them):

gacutil /i AssemblyInitCustomDomainManager.dll
gacutil /l AssemblyInitCustomDomainManager

Now edit the WcfServiceHost.exe.config (typically located at: C:\Program Files\Microsoft Visual Studio 10.0\Common7\IDE or the x86 version on 64 bit systems) and add the following inside the runtime element (see here for info about this settings):

<appDomainManagerType value="AssemblyInitCustomDomainManager.CustomManager" />
<appDomainManagerAssembly value="AssemblyInitCustomDomainManager, Version=1.0.0.0, Culture=neutral, PublicKeyToken=c841b3549556e52a, processorArchitecture=MSIL" />

NOTE: You will need to change at least one of (and possibly all depending on what you called things above): type name, namespace, assembly name, public key, version number. I guess you should be able to figure out what they need to be in your case.

OK, that was easy enough, now we will create a new "WCF Service Library" project in visual studio and it will create an app.config and a service for us (this is the project type you are wanting to test right, oh I hope so!).

First of all, remove the system.servicemodel part from the app.config since we do not want the service host application to read this in and then delete the Service1.cs and IService1.cs (as I am going to make my own a bit later). Make sure that you reference the app domain manager assembly you created earlier since you need to implement that interface.

Now, create a new file and stick the following code in (we are just having a simple service with a dependency hosted by the Castle WCF Facility):

using System;
using System.ServiceModel;

using AssemblyInitCustomDomainManager;

using Castle.Facilities.WcfIntegration;
using Castle.MicroKernel.Registration;
using Castle.MicroKernel.SubSystems.Configuration;
using Castle.Windsor;
using Castle.Windsor.Installer;

namespace TestWcfServiceLibrary
{
    public class AssemblyInitializedHandler : IAssemblyLoaded
    {
        // This method is called when the assembly loads so we will create the
        // windsor container and run all the installers we find
        public void AssemblyLoaded()
        {            
            new WindsorContainer().Install(FromAssembly.This());            
        }        
    }

    // This installer will set up the services
    public class ServicesInstaller : IWindsorInstaller
    {
        public void Install(IWindsorContainer container, 
                            IConfigurationStore store)
        {
            container
                .AddFacility<WcfFacility>(f => f.CloseTimeout = TimeSpan.Zero)
                .Register(Component
                              .For<ITestDependency>()
                              .ImplementedBy<TestDependency>(),
                          Component
                              .For<IService1>()
                              .ImplementedBy<Service1>()
                              .AsWcfService(new DefaultServiceModel(WcfEndpoint
                                                                        .BoundTo(new WSHttpBinding()))
                                                .AddBaseAddresses("http://localhost:9777/TestWcfServiceLibrary/Service1")
                                                .PublishMetadata(m => m.EnableHttpGet())));
        }
    }

    // This is the contract of something we want to make sure is loaded
    // by Windsor
    public interface ITestDependency
    {
        int DoSomething(int value);
    }

    public class TestDependency : ITestDependency
    {
        public int DoSomething(int value)
        {
            return value;
        }
    }

    // Regular WCF service contract
    [ServiceContract]
    public interface IService1
    {
        [OperationContract]
        string GetData(int value);
    }

    // Service implementation - notice it does not have a default 
    // constructor
    public class Service1 : IService1
    {
        private readonly ITestDependency _td;

        public Service1(ITestDependency td)
        {
            _td = td;
        }

        public string GetData(int value)
        {
            int v = _td.DoSomething(value);
            return string.Format(
                "According to our dependency you entered: {0}", v);
        }
    }
}

Click run and you will get an error message that says:

WCF Service Host cannot find any service metadata. This may cause the client application to run improperly. Please check if metadata is enabled. Do you want to exit?

Don't worry, just click no.

The test client starts up, but sadly it does not have your service in it. No worries, just right-click add service... and put in the URL of the service (it is in the installer in the code - http://localhost:9777/TestWcfServiceLibrary/Service1).

And there you go - WCF service hosted inside the WCF Test Client. Don't believe me - test it out, invoke the GetData operation and you should see a result.

There you go. Now if you asked if this is a good idea...I do not know but it works and I think it is what you asked for...



回答2:

I remember reading this somewhere! link

"The biggest drawback of WcfSvcHost is that it is only suitable for simple scenarios where you do not require programmatic access to the host instance before opening it or programmatic access to its event model once opened. Unlike hosting with IIS or the Windows Activation Service (WAS), there is no equivalent service host factory support. Consequently, there is no ability to dynamically add base addresses, configure endpoints, throttle calls, configure custom behaviors at the host level, and so on. My experience with WCF is that in all but the simplest cases, eventually you will need programmatic access to the host instance, so I do not view WcfSvcHost as a full-fledged production-worthy host, as I do the WAS or a dedicated self host."