Can I automatically host all services in app.config when using SelfHosting?
Asked Answered
A

3

7

I'm writing an application which needs to host several WCF services. One of the strengths of WCF is the ability to configure services without having to recompile, by specifying settings in the app.config file.

When self-hosting, there does not appear to be an out-of-the-box way to automatically host services which are in the app.config file. I found this question which mentions a possible solution of dynamically enumerating services listed in app.config at runtime, and creating a ServiceHost for each.

However, my services, contracts, and the hosting application are all in different assemblies. This causes Type.GetType(string name) to fails to locate my service type (returns null) because it is defined in a different assembly.

How can I reliably host all services listed in the app.config file dynamically (i.e., without hard-coding new ServiceHost(typeof(MyService)) in my self-hosting application?

Note: My app.config was generated using the "WCF Configuration Editor" in Visual Studio 2010.

Note also: My primary goal is to have this driven by the app.config file so there is a single point of configuration. I don't want to have to configure this in a separate location.

EDIT: I am able to read the app.config file (see here), but need to be able to resolve types in different assemblies.

EDIT: One of the answers below prompted me to try specifying the AssemblyQualifiedName in app.config instead of just the basic type name. This was able to get around the Type.GetType() problem, however ServiceHost.Open() now fails with an InvalidOperationException regardless of how I attain the type:

// Fails
string typeName = typeof(MyService).AssemblyQualifiedName;
Type myType = Type.GetType(typeName);
ServiceHost host = new ServiceHost(myType);
host.Open(); // throws InvalidOperationException

// Also fails
Type myType2 = typeof(MyService);
ServiceHost host2 = new ServiceHost(myType2);
host2.Open(); // throws InvalidOperationException

Exception details:

Service 'SO.Example.MyService' has zero application (non-infrastructure) endpoints. This might be because no configuration file was found for your application, or because no service element matching the service name could be found in the configuration file, or because no endpoints were defined in the service element.

I guess WCF attempts to match the literal string for the service name when parsing the app.config file internally.

EDIT/ANSWER: What I ended up doing was basically what was in the answer below. Instead of using Type.GetType() I know that all of my services are in the same assembly, so I switched to:

// Get a reference to the assembly which contain all of the service implementations.
Assembly implementationAssembly = Assembly.GetAssembly(typeof(MyService));
...
// When loading the type for the service, load it from the implementing assembly.
Type implementation = implementationAssembly.GetType(serviceElement.Name);
Aluminate answered 13/5, 2011 at 14:41 Comment(2)
Yes, the service's name= attribute value must match exactly to the fully-qualified name of the class implementing that WCF service. No fiddling / "extending" of that attribute allowed....Jerusalem
That's a pity, as it would be useful to allow an assembly qualified name, then I wouldn't need to do anything extra.Aluminate
E
1

You correctly identified the answer to your problem in your question link and @marc_s answer also gives the correct approach too. The actual issue you are having is that you need to dynamically get the Type instance of an assembly that may only be referenced through a config file so it may not be loaded into the current AppDomain.

Look at this blog post for a way to dynamically reference assemblies in your code. Although the post is specifically for an ASP.NET application, the general approach should work in a self hosted scenario. The ideas is to replace the Type.GetType(string) call with a private method call that dynamically loads the requested assembly (if needed) and returns the Type object. The parameter you send this method will still be the element.Name and you'll need to figure out which is the correct assembly to load. A simple convention-based assembly naming scheme should work. For example, if service type is:

MyNamespace.MyService.MyServiceImpl

then assume the assembly is:

MyNamespace.MyService

Erfurt answered 13/5, 2011 at 16:20 Comment(11)
I'm aware of the actual issue, that's why I included it in my question description. The blog post you reference is interesting and informative, though seems rather heavy-handed. The suggestion to dynamically load assembly based on namespace seems reasonable. Thinking about it more, I wonder if I could put an assembly qualified type name (Type.AssemblyQualifiedName, including assembly reference) in the app.config file instead of the default soft type name.Aluminate
Hope that works. If it doesn't there is a way to load assemblies from the config file by using the configuration runtime element to set up the dependentAssembly element for the app. That would take care of loading the assemblies into the AppDomain. You'd just need to use reflection to get the Type instance for the service you're spinning up.Erfurt
Didn't work. Type.GetType() was able to load the type, however ServiceHost.Open() failed when created with it.Aluminate
What was the exception thrown when .Open() failed?Erfurt
Updated question with more details. It looks like my code is now able to locate the type, but now WCF cannot locate the Service in app.config that matches (presumably it is looking for the sorter form of the type name). Arg.Aluminate
Did you also add the dependentAssembly configuration into app.config file? Just being able to reference an assembly in code doesn't necessarily guarantee that it will be loaded. The dependentAssembly config will load the assemblies into the current AppDomain which I assume is the same one that WCF is running in.Erfurt
I think this is orthogonal to the AssemblyQualifiedName. marc_s (above) pointed out that the service name must be the FullName, and nothing more or less. I tried adding the dependentAssembly to app.config, but was not able to get any further: <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="SO.Example" publicKeyToken="332c29026952d602" culture="neutral" /> </dependentAssembly> </assemblyBinding> </runtime>Aluminate
Does the SO.Example assembly show in this: AppDomain.CurrentDomain.GetAssemblies() list? If so then its available and there is another issue. Otherwise, the dependentAssembly config isn't working. It seems correct from what I can tell.Erfurt
SO.Example is listed in AppDomain.CurrentDomain.GetAssemblies(). The same is true even without the dependentAssembly config, though.Aluminate
Based on MSDN documentation for GetType, it looks like this requires an AssemblyQualifiedName unless the type is either in the executing assembly, or in mscorlib. [msdn.microsoft.com/en-us/library/w3f99sx1.aspx]Aluminate
Sorry, the suggestions didn't help. From the exception, it seems WCF isn't able to configure the service automatically because of that fully qualified name issue. You try could using something like AppConfig to store the necessary service config info and then configure the service in code using those values before adding it the the ServiceHost constructor. That's pretty hacky though.Erfurt
T
5
    // get the <system.serviceModel> / <services> config section
    ServicesSection services = ConfigurationManager.GetSection("system.serviceModel/services") as ServicesSection;

    // get all classs
    var allTypes = AppDomain.CurrentDomain.GetAssemblies().ToList().SelectMany(s => s.GetTypes()).Where(t => t.IsClass == true);

    // enumerate over each <service> node
    foreach (ServiceElement service in services.Services)
    {
        Type serviceType = allTypes.SingleOrDefault(t => t.FullName == service.Name);
        if (serviceType == null)
        {
            continue;
        }

        ServiceHost serviceHost = new ServiceHost(serviceType);
        serviceHost.Open();
    }

Based on the other answers, I extended the code to the following, which searches all assemblies for the services in the app.config

Tetraploid answered 21/10, 2012 at 16:25 Comment(1)
Type.GetType does this already. The reason it fails is because the Assembly itself has not been loaded yet.Aluminate
J
4

That should definitely be possible ! Check out this code fragment - use it as a foundation and go from here:

using System.Configuration;   // don't forget to add a reference to this assembly!

// get the <system.serviceModel> / <services> config section
ServicesSection services = ConfigurationManager.GetSection("system.serviceModel/services") as ServicesSection;

// enumerate over each <service> node
foreach(ServiceElement aService in services.Services)
{
    Console.WriteLine();
    Console.WriteLine("Name: {0} / Behavior: {1}", aService.Name, aService.BehaviorConfiguration);

    // enumerate over all endpoints for that service
    foreach (ServiceEndpointElement see in aService.Endpoints)
    {
        Console.WriteLine("\tEndpoint: Address = {0} / Binding = {1} / Contract = {2}", see.Address, see.Binding, see.Contract);
    }
}

This right now just prints out the info - but you could definitely use this to actually build up your service hosts inside your own NT service !

Update: ok, sorry, I missed your most important point - the fact the actual services are in different assemblies.

In that case, you need to dynamically load those assemblies, as needed - you could e.g. "just simply know" what assemblies to load, or you could put them all into a specific subdirectories and load all assemblies in that directory, or you could just inspect all assemblies in the same location where your MyOwnServiceHost.exe resides and check if you find any types that you need.

This part - which service type to find in which assembly - isn't handled by WCF configuration - you need to do this yourself, by whichever means makes most sense to you.

// find currently executing assembly
Assembly curr = Assembly.GetExecutingAssembly();

// get the directory where this app is running in
string currentLocation = Path.GetDirectoryName(curr.Location);

// find all assemblies inside that directory
string[] assemblies = Directory.GetFiles(currentLocation, "*.dll");

// enumerate over those assemblies
foreach (string assemblyName in assemblies)
{
   // load assembly just for inspection
   Assembly assemblyToInspect = Assembly.ReflectionOnlyLoadFrom(assemblyName);

   if (assemblyToInspect != null)
   {
      // find all types
      Type[] types = assemblyToInspect.GetTypes();

      // enumerate types and determine if this assembly contains any types of interest
      // you could e.g. put a "marker" interface on those (service implementation)
      // types of interest, or you could use a specific naming convention (all types
      // like "SomeThingOrAnotherService" - ending in "Service" - are your services)
      // or some kind of a lookup table (e.g. the list of types you need to find from
      // parsing the app.config file)
      foreach(Type ty in types)
      {
         // do something here
      }
   }
}
Jerusalem answered 13/5, 2011 at 15:11 Comment(1)
I fail to see how this is any different than the question I linked. Am I missing something? My primary problem at this point is the fact that Type.GetType(string name) returns null. I am already able to find the type name from the app.config file.Aluminate
E
1

You correctly identified the answer to your problem in your question link and @marc_s answer also gives the correct approach too. The actual issue you are having is that you need to dynamically get the Type instance of an assembly that may only be referenced through a config file so it may not be loaded into the current AppDomain.

Look at this blog post for a way to dynamically reference assemblies in your code. Although the post is specifically for an ASP.NET application, the general approach should work in a self hosted scenario. The ideas is to replace the Type.GetType(string) call with a private method call that dynamically loads the requested assembly (if needed) and returns the Type object. The parameter you send this method will still be the element.Name and you'll need to figure out which is the correct assembly to load. A simple convention-based assembly naming scheme should work. For example, if service type is:

MyNamespace.MyService.MyServiceImpl

then assume the assembly is:

MyNamespace.MyService

Erfurt answered 13/5, 2011 at 16:20 Comment(11)
I'm aware of the actual issue, that's why I included it in my question description. The blog post you reference is interesting and informative, though seems rather heavy-handed. The suggestion to dynamically load assembly based on namespace seems reasonable. Thinking about it more, I wonder if I could put an assembly qualified type name (Type.AssemblyQualifiedName, including assembly reference) in the app.config file instead of the default soft type name.Aluminate
Hope that works. If it doesn't there is a way to load assemblies from the config file by using the configuration runtime element to set up the dependentAssembly element for the app. That would take care of loading the assemblies into the AppDomain. You'd just need to use reflection to get the Type instance for the service you're spinning up.Erfurt
Didn't work. Type.GetType() was able to load the type, however ServiceHost.Open() failed when created with it.Aluminate
What was the exception thrown when .Open() failed?Erfurt
Updated question with more details. It looks like my code is now able to locate the type, but now WCF cannot locate the Service in app.config that matches (presumably it is looking for the sorter form of the type name). Arg.Aluminate
Did you also add the dependentAssembly configuration into app.config file? Just being able to reference an assembly in code doesn't necessarily guarantee that it will be loaded. The dependentAssembly config will load the assemblies into the current AppDomain which I assume is the same one that WCF is running in.Erfurt
I think this is orthogonal to the AssemblyQualifiedName. marc_s (above) pointed out that the service name must be the FullName, and nothing more or less. I tried adding the dependentAssembly to app.config, but was not able to get any further: <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="SO.Example" publicKeyToken="332c29026952d602" culture="neutral" /> </dependentAssembly> </assemblyBinding> </runtime>Aluminate
Does the SO.Example assembly show in this: AppDomain.CurrentDomain.GetAssemblies() list? If so then its available and there is another issue. Otherwise, the dependentAssembly config isn't working. It seems correct from what I can tell.Erfurt
SO.Example is listed in AppDomain.CurrentDomain.GetAssemblies(). The same is true even without the dependentAssembly config, though.Aluminate
Based on MSDN documentation for GetType, it looks like this requires an AssemblyQualifiedName unless the type is either in the executing assembly, or in mscorlib. [msdn.microsoft.com/en-us/library/w3f99sx1.aspx]Aluminate
Sorry, the suggestions didn't help. From the exception, it seems WCF isn't able to configure the service automatically because of that fully qualified name issue. You try could using something like AppConfig to store the necessary service config info and then configure the service in code using those values before adding it the the ServiceHost constructor. That's pretty hacky though.Erfurt

© 2022 - 2024 — McMap. All rights reserved.