creating WCF ChannelFactory<T>
Asked Answered
F

4

47

I'm trying to convert an existing .NET Remoting application to WCF. Both server and client share common interface and all objects are server-activated objects.

In WCF world, this would be similar to creating per-call service and using ChannelFactory<T> to create a proxy. I'm struggling a bit with how to properly create ChannelFactory<T> for an ASP.NET client.

For performance reasons, I want to cache ChannelFactory<T> objects and just create channel every time I call the service. In .NET remoting days, there used to be RemotingConfiguration.GetRegisteredWellknownClientTypes() method to get a collection of client objects that I could then cache. It appears, in WCF world there is no such thing, although I was able to get a collection of endpoints from config file.

Now here is what I think will work. I can create something like this:

public static ProxyHelper
{
    static Dictionary<Type, object> lookup = new Dictionary<string, object>();  

    static public T GetChannel<T>()
    {
        Type type = typeof(T);
        ChannelFactory<T> factory;

        if (!lookup.ContainsKey(type))
        {
            factory = new ChannelFactory<T>();
            lookup.Add(type, factory);
        }
        else
        {
            factory = (ChannelFactory<T>)lookup[type];
        }

        T proxy = factory.CreateChannel();   
        ((IClientChannel)proxy).Open();

        return proxy;
    }    
}

I think the above code will work, but I'm a bit worried about multiple threads trying to add new ChannelFactory<T> objects if it's not in the lookup. Since I'm using .NET 4.0, I was thinking about using ConcurrentDictionary and use GetOrAdd() method or use TryGetValue() method first to check if ChannelFactory<T> exists and it does not exist, then use GetOrAdd() method. Not sure about performance though of ConcurrentDictionary.TryGetValue() and ConcurrentDictionary.GetOrAdd() method.

Another minor question is whether I need to call ChannelFactory.Close() method on channel factory objects after ASP.NET application ends or can I just let .NET framework dispose the channel factory objects on its own. The proxy channel will always be closed after calling service method by using ((IChannel)proxy).Close() method.

Farmann answered 8/7, 2010 at 1:50 Comment(0)
V
14

Yes, if you want to create something like this - a static class to hold all those ChannelFactory<T> instances - you definitely have to make sure this class is 100% thread-safe and cannot stumble when accessed concurrently. I haven't used .NET 4's features much yet, so I cannot comment on those specifically - but I would definitely recommend to make this as safe as possible.

As for your second (minor) question: the ChannelFactory itself is a static class - so you cannot really call a .Close() method on it. If you meant to ask whether or not to call the .Close() method on the actual IChannel, then again: yes, try your best to be a good citizen and close those channels if you ever can. If you miss one, .NET will take care of it - but don't just toss your unused channels on the floor and go on - clean up after yourself! :-)

Vigilantism answered 8/7, 2010 at 5:22 Comment(1)
Sorry, I meant closing instance of the ChannelFactory<T> class. The dictionary will contain all the instances of the ChannelFactory<T>, but I want to close each factory at some point when the app no longer needs them. I played around with AppDomain.CurrentDomain.DomainUnload event, which seems like it will work for my case. I was able to subscribe to DomainUnload event and close each channel factory when ASP .NET application domain restarts. Still, I'm not sure whether to use ConcurrentDictionary in .NET 4 or use something like Darrin has below where access to dictionary is synchronized.Farmann
K
69

Here's a helper class that I use to handle channel factories:

public class ChannelFactoryManager : IDisposable
{
    private static Dictionary<Type, ChannelFactory> _factories = new Dictionary<Type,ChannelFactory>();
    private static readonly object _syncRoot = new object();

    public virtual T CreateChannel<T>() where T : class
    {
        return CreateChannel<T>("*", null);
    }

    public virtual T CreateChannel<T>(string endpointConfigurationName) where T : class
    {
        return CreateChannel<T>(endpointConfigurationName, null);
    }

    public virtual T CreateChannel<T>(string endpointConfigurationName, string endpointAddress) where T : class
    {
        T local = GetFactory<T>(endpointConfigurationName, endpointAddress).CreateChannel();
        ((IClientChannel)local).Faulted += ChannelFaulted;
        return local;
    }

    protected virtual ChannelFactory<T> GetFactory<T>(string endpointConfigurationName, string endpointAddress) where T : class
    {
        lock (_syncRoot)
        {
            ChannelFactory factory;
            if (!_factories.TryGetValue(typeof(T), out factory))
            {
                factory = CreateFactoryInstance<T>(endpointConfigurationName, endpointAddress);
                _factories.Add(typeof(T), factory);
            }
            return (factory as ChannelFactory<T>);
        }
    }

    private ChannelFactory CreateFactoryInstance<T>(string endpointConfigurationName, string endpointAddress)
    {
        ChannelFactory factory = null;
        if (!string.IsNullOrEmpty(endpointAddress))
        {
            factory = new ChannelFactory<T>(endpointConfigurationName, new EndpointAddress(endpointAddress));
        }
        else
        {
            factory = new ChannelFactory<T>(endpointConfigurationName);
        }
        factory.Faulted += FactoryFaulted;
        factory.Open();
        return factory;
    }

    private void ChannelFaulted(object sender, EventArgs e)
    {
        IClientChannel channel = (IClientChannel)sender;
        try
        {
            channel.Close();
        }
        catch
        {
            channel.Abort();
        }
        throw new ApplicationException("Exc_ChannelFailure");
    }

    private void FactoryFaulted(object sender, EventArgs args)
    {
        ChannelFactory factory = (ChannelFactory)sender;
        try
        {
            factory.Close();
        }
        catch
        {
            factory.Abort();
        }
        Type[] genericArguments = factory.GetType().GetGenericArguments();
        if ((genericArguments != null) && (genericArguments.Length == 1))
        {
            Type key = genericArguments[0];
            if (_factories.ContainsKey(key))
            {
                _factories.Remove(key);
            }
        }
        throw new ApplicationException("Exc_ChannelFactoryFailure");
    }

    public void Dispose()
    {
        Dispose(true);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            lock (_syncRoot)
            {
                foreach (Type type in _factories.Keys)
                {
                    ChannelFactory factory = _factories[type];
                    try
                    {
                        factory.Close();
                        continue;
                    }
                    catch
                    {
                        factory.Abort();
                        continue;
                    }
                }
                _factories.Clear();
            }
        }
    }
}

Then I define a service invoker:

public interface IServiceInvoker
{
    R InvokeService<T, R>(Func<T, R> invokeHandler) where T: class;
}

and an implementation:

public class WCFServiceInvoker : IServiceInvoker
{
    private static ChannelFactoryManager _factoryManager = new ChannelFactoryManager();
    private static ClientSection _clientSection = ConfigurationManager.GetSection("system.serviceModel/client") as ClientSection;

    public R InvokeService<T, R>(Func<T, R> invokeHandler) where T : class
    {
        var endpointNameAddressPair = GetEndpointNameAddressPair(typeof(T));
        T arg = _factoryManager.CreateChannel<T>(endpointNameAddressPair.Key, endpointNameAddressPair.Value);
        ICommunicationObject obj2 = (ICommunicationObject)arg;
        try
        {
            return invokeHandler(arg);
        }
        finally
        {
            try
            {
                if (obj2.State != CommunicationState.Faulted)
                {
                    obj2.Close();
                }
            }
            catch
            {
                obj2.Abort();
            }
        }
    }

    private KeyValuePair<string, string> GetEndpointNameAddressPair(Type serviceContractType)
    {
        var configException = new ConfigurationErrorsException(string.Format("No client endpoint found for type {0}. Please add the section <client><endpoint name=\"myservice\" address=\"http://address/\" binding=\"basicHttpBinding\" contract=\"{0}\"/></client> in the config file.", serviceContractType));
        if (((_clientSection == null) || (_clientSection.Endpoints == null)) || (_clientSection.Endpoints.Count < 1))
        {
            throw configException;
        }
        foreach (ChannelEndpointElement element in _clientSection.Endpoints)
        {
            if (element.Contract == serviceContractType.ToString())
            {
                return new KeyValuePair<string, string>(element.Name, element.Address.AbsoluteUri);
            }
        }
        throw configException;
    }

}

Now every time you need to call a WCF service you could use this:

WCFServiceInvoker invoker = new WCFServiceInvoker();
SomeReturnType result = invoker.InvokeService<IMyServiceContract, SomeReturnType>(
    proxy => proxy.SomeMethod()
);

This assumes that you've defined a client endpoint for the IMyServiceContract service contract in the config file:

<client>
    <endpoint 
        name="myservice" 
        address="http://example.com/" 
        binding="basicHttpBinding" 
        contract="IMyServiceContract" />
</client>
Kobylak answered 8/7, 2010 at 5:57 Comment(14)
Thanks! This is very interesting, I'll need to analyze it a bit more. I have couple questions: you are subscribing to ChannelFactory<T>.Faulted event. Is there a need for that? When will that event fire and is subscribing to Channel.Faulted is enough to detect faulted state?Farmann
Hi, great solution! I noticed one funny thing with the segment "return new KeyValuePair<string, string>(element.Name, element.Address.Host);" should really be "return new KeyValuePair<string, string>(element.Name, element.Address.AbsoluteUri);"Frigate
@Tri Q, you are absolutely correct. I've wrongly copy-pasted from a code I used which had some more stuff and forgot to replace :-) I've updated my post. Thanks for pointing this out.Kobylak
What about this:#5634041 ? please, can modify your class for load different-files-on-the-client-side ?Phytopathology
Thanks for this Darin... started using it in a project I'm working on. Added a method to IServiceInvoker for Action<T>, since Func<T, R> can't execute void methods. Have you updated this on your end at all after running into any situations?Shepperd
@Langdon, what issues are you encountering with this? It shouldn't be difficult to add such method. In the implementation it would simply invoke the more general method and it will not return a result.Kobylak
Darin, sorry that was poorly worded -- no problems overloading InvokeService to void/Action<T>. I meant: Have you had any additional hiccups with this implementation that you've encountered since pasting it here last November? Any changes that someone grabbing the November version might benefit from? Thanks!Shepperd
You can avoid the dictionary lookup in ChannelFactoryManager if you make it a generic class. Then you can just have a static _factory reference of type ChannelFactory<T>. This would avoid the need for locking. The disadvantage is that you would have to dispose each ChannelFactoryManager type individually.Allocution
@DarinDimitrov - Thank you for this implementation - I had implemented something comparable, however not quite as fully featured as yours. I have a question though: suppose the need to create channels based on different configurations for the same contract? (i.e. a service with multiple endpoints.)Finisterre
Is it a typo to have your dictionary of factories (_factories) as a static variable but have the instance's dispose function clean it up?Gramnegative
I don't understand, what is the point of calling Close() in Faulted event handlers ? Isn't it going to call Abort() internally ?Brophy
When you subscribe to this event ((IClientChannel)local).Faulted += ChannelFaulted, the return invokeHandler(arg); line won't throw an exception by default. I guess that's why throw new ApplicationException("Exc_ChannelFailure"); was added. Has anyone found a way to get the original exception?Baker
@DarinDimitrov We're currently using this in a few projects of ours. However, a service supplied to us requires that ClientCredentials in the form of a user name and password. The only place I can find to set these up is inside private ChannelFactory CreateFactoryInstance<T> before the factory is opened (due to it becoming immutable). However, seeing as this is a web application, I'm afraid this would cause some concurrency problems (overwriting client-specific credentials on a shared factory). Can you think of a way to prevent this from happening?Kato
@NelsonRothermel, in my tests exceptions are thrown from return invokeHandler(arg). I ended up removing throw new ApplicationException("Exc_ChannelFailure");, since it masks the original exception and I do not understand the need for throwing it. Otherwise @DarinDimitrov's implementation has worked well for my purposes.Ancestor
V
14

Yes, if you want to create something like this - a static class to hold all those ChannelFactory<T> instances - you definitely have to make sure this class is 100% thread-safe and cannot stumble when accessed concurrently. I haven't used .NET 4's features much yet, so I cannot comment on those specifically - but I would definitely recommend to make this as safe as possible.

As for your second (minor) question: the ChannelFactory itself is a static class - so you cannot really call a .Close() method on it. If you meant to ask whether or not to call the .Close() method on the actual IChannel, then again: yes, try your best to be a good citizen and close those channels if you ever can. If you miss one, .NET will take care of it - but don't just toss your unused channels on the floor and go on - clean up after yourself! :-)

Vigilantism answered 8/7, 2010 at 5:22 Comment(1)
Sorry, I meant closing instance of the ChannelFactory<T> class. The dictionary will contain all the instances of the ChannelFactory<T>, but I want to close each factory at some point when the app no longer needs them. I played around with AppDomain.CurrentDomain.DomainUnload event, which seems like it will work for my case. I was able to subscribe to DomainUnload event and close each channel factory when ASP .NET application domain restarts. Still, I'm not sure whether to use ConcurrentDictionary in .NET 4 or use something like Darrin has below where access to dictionary is synchronized.Farmann
G
2

I didn't like the calling construction:

WCFServiceInvoker invoker = new WCFServiceInvoker();
var result = invoker.InvokeService<IClaimsService, ICollection<string>>(proxy => proxy.GetStringClaims());

Also you cannot use the same channel twice.

I've created this solution:

using(var i = Connection<IClaimsService>.Instance)
{           
   var result = i.Channel.GetStringClaims();
}

Now you can reuse the same channel until the using statement calls the dispose.

The GetChannel method is basicly a ChannelFactory.CreateChannel() with some extra config's I'm using.

You could build some caching for the ChannelFactory's as the other solutions does.

Code for the Connnection class:

public static class Connection<T>
   {
      public static ChannelHolder Instance
      {
         get
         {
            return new ChannelHolder();
         }
      }

      public class ChannelHolder : IDisposable
      {
         public T Channel { get; set; }

         public ChannelHolder()
         {
            this.Channel = GetChannel();
         }

         public void Dispose()
         {
            IChannel connection = null;
            try
            {
               connection = (IChannel)Channel;
               connection.Close();
            }
            catch (Exception)
            {
               if (connection != null)
               {
                  connection.Abort();
               }
            }
         }
      }
}
Godewyn answered 29/4, 2011 at 14:34 Comment(1)
I think this won't work if you want to add central logic such as auto-retrying a request. You can subscribe to ICommunicationObject.Faulted, but then the original exception won't throw.Baker
C
0

@NelsonRothermel, yes I went down the road of not using a try catch in the ChannelFactoryManager ChannelFaulted event handler. So ChannelFaulted would become

 private void ChannelFaulted(object sender, EventArgs e)
    {
        IClientChannel channel = (IClientChannel)sender;            
        channel.Abort();
    }

Seems to allow the original exception to bubble up. Also chose not to use channel.close as it seems to throw an exception as the channel is in a faulted state already. FactoryFaulted event handler may have similar issues. Btw @Darin, good bit of code...

Crosscrosslet answered 30/5, 2014 at 9:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.