What is the best workaround for the WCF client `using` block issue?

前端 未结 26 1723
余生分开走
余生分开走 2020-11-22 00:03

I like instantiating my WCF service clients within a using block as it\'s pretty much the standard way to use resources that implement IDisposable:

相关标签:
26条回答
  • 2020-11-22 00:16

    Actually, although I blogged (see Luke's answer), I think this is better than my IDisposable wrapper. Typical code:

    Service<IOrderService>.Use(orderService=>
    {
      orderService.PlaceOrder(request);
    }); 
    

    (edit per comments)

    Since Use returns void, the easiest way to handle return values is via a captured variable:

    int newOrderId = 0; // need a value for definite assignment
    Service<IOrderService>.Use(orderService=>
      {
        newOrderId = orderService.PlaceOrder(request);
      });
    Console.WriteLine(newOrderId); // should be updated
    
    0 讨论(0)
  • 2020-11-22 00:16

    Summary

    Using the techniques described in this answer one can consume a WCF service in a using block with the following syntax:

    var channelFactory = new ChannelFactory<IMyService>("");
    
    var serviceHelper = new ServiceHelper<IMyService>(channelFactory);
    var proxy = serviceHelper.CreateChannel();
    using (proxy as IDisposable)
    {
        proxy.DoWork();
    }
    

    You can of course adapt this even further to achieve a more concise programming model specific to your situation - but the point is that we can create an implementation of IMyService reprenting the channel which correctly implements the disposable pattern.


    Details

    All the answers given thus far address the problem of getting around the "bug" in the WCF Channel implemention of IDisposable. The answer that seems to offer the most concise programming model (allowing you to use the using block to dispose on unmanaged resources) is this one - where the proxy is modifed to implement IDisposable with a bug-free implementation. The problem with this approach is maintainability - we have to re-implement this functionality for ever proxy we use. On a variation of this answer we will see how we can use composition rather than inheritance to make this technique generic.

    First Attempt

    There seem to various implementations for the IDisposable implementation, but for sake of argument we will use an adaption of that used by the currently accepted answer.

    [ServiceContract]
    public interface IMyService
    {
        [OperationContract]
        void DoWork();
    }
    
    public class ProxyDisposer : IDisposable
    {
        private IClientChannel _clientChannel;
    
    
        public ProxyDisposer(IClientChannel clientChannel)
        {
            _clientChannel = clientChannel;
        }
    
        public void Dispose()
        {
            var success = false;
            try
            {
                _clientChannel.Close();
                success = true;
            }
            finally
            {
                if (!success)
                    _clientChannel.Abort();
                _clientChannel = null;
            }
        }
    }
    
    public class ProxyWrapper : IMyService, IDisposable
    {
        private IMyService _proxy;
        private IDisposable _proxyDisposer;
    
        public ProxyWrapper(IMyService proxy, IDisposable disposable)
        {
            _proxy = proxy;
            _proxyDisposer = disposable;
        }
    
        public void DoWork()
        {
            _proxy.DoWork();
        }
    
        public void Dispose()
        {
            _proxyDisposer.Dispose();
        }
    }
    

    Armed with the above classes we can now write

    public class ServiceHelper
    {
        private readonly ChannelFactory<IMyService> _channelFactory;
    
        public ServiceHelper(ChannelFactory<IMyService> channelFactory )
        {
            _channelFactory = channelFactory;
        }
    
        public IMyService CreateChannel()
        {
            var channel = _channelFactory.CreateChannel();
            var channelDisposer = new ProxyDisposer(channel as IClientChannel);
            return new ProxyWrapper(channel, channelDisposer);
        }
    }
    

    This allows us to consume our service using the using block:

    ServiceHelper serviceHelper = ...;
    var proxy = serviceHelper.CreateChannel();
    using (proxy as IDisposable)
    {
        proxy.DoWork();
    }
    

    Making this generic

    All we have done so far is to reformulate Tomas' solution. What prevents this code from being generic is the fact that ProxyWrapper class has to be re-implemented for every service contract we want. We will now look at a class that allows us to create this type dynamically using IL:

    public class ServiceHelper<T>
    {
        private readonly ChannelFactory<T> _channelFactory;
    
        private static readonly Func<T, IDisposable, T> _channelCreator;
    
        static ServiceHelper()
        {
            /** 
             * Create a method that can be used generate the channel. 
             * This is effectively a compiled verion of new ProxyWrappper(channel, channelDisposer) for our proxy type
             * */
            var assemblyName = Guid.NewGuid().ToString();
            var an = new AssemblyName(assemblyName);
            var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run);
            var moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName);
    
            var proxyType = CreateProxyType(moduleBuilder, typeof(T), typeof(IDisposable));
    
            var channelCreatorMethod = new DynamicMethod("ChannelFactory", typeof(T),
                new[] { typeof(T), typeof(IDisposable) });
    
            var ilGen = channelCreatorMethod.GetILGenerator();
            var proxyVariable = ilGen.DeclareLocal(typeof(T));
            var disposableVariable = ilGen.DeclareLocal(typeof(IDisposable));
            ilGen.Emit(OpCodes.Ldarg, proxyVariable);
            ilGen.Emit(OpCodes.Ldarg, disposableVariable);
            ilGen.Emit(OpCodes.Newobj, proxyType.GetConstructor(new[] { typeof(T), typeof(IDisposable) }));
            ilGen.Emit(OpCodes.Ret);
    
            _channelCreator =
                (Func<T, IDisposable, T>)channelCreatorMethod.CreateDelegate(typeof(Func<T, IDisposable, T>));
    
        }
    
        public ServiceHelper(ChannelFactory<T> channelFactory)
        {
            _channelFactory = channelFactory;
        }
    
        public T CreateChannel()
        {
            var channel = _channelFactory.CreateChannel();
            var channelDisposer = new ProxyDisposer(channel as IClientChannel);
            return _channelCreator(channel, channelDisposer);
        }
    
       /**
        * Creates a dynamic type analogous to ProxyWrapper, implementing T and IDisposable.
        * This method is actually more generic than this exact scenario.
        * */
        private static Type CreateProxyType(ModuleBuilder moduleBuilder, params Type[] interfacesToInjectAndImplement)
        {
            TypeBuilder tb = moduleBuilder.DefineType(Guid.NewGuid().ToString(),
                TypeAttributes.Public | TypeAttributes.Class);
    
            var typeFields = interfacesToInjectAndImplement.ToDictionary(tf => tf,
                tf => tb.DefineField("_" + tf.Name, tf, FieldAttributes.Private));
    
            #region Constructor
    
            var constructorBuilder = tb.DefineConstructor(
                MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName |
                MethodAttributes.RTSpecialName,
                CallingConventions.Standard,
                interfacesToInjectAndImplement);
    
            var il = constructorBuilder.GetILGenerator();
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Call, typeof(object).GetConstructor(new Type[0]));
    
            for (var i = 1; i <= interfacesToInjectAndImplement.Length; i++)
            {
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldarg, i);
                il.Emit(OpCodes.Stfld, typeFields[interfacesToInjectAndImplement[i - 1]]);
            }
            il.Emit(OpCodes.Ret);
    
            #endregion
    
            #region Add Interface Implementations
    
            foreach (var type in interfacesToInjectAndImplement)
            {
                tb.AddInterfaceImplementation(type);
            }
    
            #endregion
    
            #region Implement Interfaces
    
            foreach (var type in interfacesToInjectAndImplement)
            {
                foreach (var method in type.GetMethods())
                {
                    var methodBuilder = tb.DefineMethod(method.Name,
                        MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig |
                        MethodAttributes.Final | MethodAttributes.NewSlot,
                        method.ReturnType,
                        method.GetParameters().Select(p => p.ParameterType).ToArray());
                    il = methodBuilder.GetILGenerator();
    
                    if (method.ReturnType == typeof(void))
                    {
                        il.Emit(OpCodes.Nop);
                        il.Emit(OpCodes.Ldarg_0);
                        il.Emit(OpCodes.Ldfld, typeFields[type]);
                        il.Emit(OpCodes.Callvirt, method);
                        il.Emit(OpCodes.Ret);
                    }
                    else
                    {
                        il.DeclareLocal(method.ReturnType);
    
                        il.Emit(OpCodes.Nop);
                        il.Emit(OpCodes.Ldarg_0);
                        il.Emit(OpCodes.Ldfld, typeFields[type]);
    
                        var methodParameterInfos = method.GetParameters();
                        for (var i = 0; i < methodParameterInfos.Length; i++)
                            il.Emit(OpCodes.Ldarg, (i + 1));
                        il.Emit(OpCodes.Callvirt, method);
    
                        il.Emit(OpCodes.Stloc_0);
                        var defineLabel = il.DefineLabel();
                        il.Emit(OpCodes.Br_S, defineLabel);
                        il.MarkLabel(defineLabel);
                        il.Emit(OpCodes.Ldloc_0);
                        il.Emit(OpCodes.Ret);
                    }
    
                    tb.DefineMethodOverride(methodBuilder, method);
                }
            }
    
            #endregion
    
            return tb.CreateType();
        }
    }
    

    With our new helper class we can now write

    var channelFactory = new ChannelFactory<IMyService>("");
    
    var serviceHelper = new ServiceHelper<IMyService>(channelFactory);
    var proxy = serviceHelper.CreateChannel();
    using (proxy as IDisposable)
    {
        proxy.DoWork();
    }
    

    Note that you could also use the same technique (with slight modifications) for auto-generated clients inheriting for ClientBase<> (instead of using ChannelFactory<>), or if you want to use a different implementation of IDisposable to close your channel.

    0 讨论(0)
  • 2020-11-22 00:17

    Below is an enhanced version of the source from the question and extended to cache multiple channel factories and attempt to look up the endpoint in the configuration file by contract name.

    It uses .NET 4 (specifically: contravariance, LINQ, var):

    /// <summary>
    /// Delegate type of the service method to perform.
    /// </summary>
    /// <param name="proxy">The service proxy.</param>
    /// <typeparam name="T">The type of service to use.</typeparam>
    internal delegate void UseServiceDelegate<in T>(T proxy);
    
    /// <summary>
    /// Wraps using a WCF service.
    /// </summary>
    /// <typeparam name="T">The type of service to use.</typeparam>
    internal static class Service<T>
    {
        /// <summary>
        /// A dictionary to hold looked-up endpoint names.
        /// </summary>
        private static readonly IDictionary<Type, string> cachedEndpointNames = new Dictionary<Type, string>();
    
        /// <summary>
        /// A dictionary to hold created channel factories.
        /// </summary>
        private static readonly IDictionary<string, ChannelFactory<T>> cachedFactories =
            new Dictionary<string, ChannelFactory<T>>();
    
        /// <summary>
        /// Uses the specified code block.
        /// </summary>
        /// <param name="codeBlock">The code block.</param>
        internal static void Use(UseServiceDelegate<T> codeBlock)
        {
            var factory = GetChannelFactory();
            var proxy = (IClientChannel)factory.CreateChannel();
            var success = false;
    
            try
            {
                using (proxy)
                {
                    codeBlock((T)proxy);
                }
    
                success = true;
            }
            finally
            {
                if (!success)
                {
                    proxy.Abort();
                }
            }
        }
    
        /// <summary>
        /// Gets the channel factory.
        /// </summary>
        /// <returns>The channel factory.</returns>
        private static ChannelFactory<T> GetChannelFactory()
        {
            lock (cachedFactories)
            {
                var endpointName = GetEndpointName();
    
                if (cachedFactories.ContainsKey(endpointName))
                {
                    return cachedFactories[endpointName];
                }
    
                var factory = new ChannelFactory<T>(endpointName);
    
                cachedFactories.Add(endpointName, factory);
                return factory;
            }
        }
    
        /// <summary>
        /// Gets the name of the endpoint.
        /// </summary>
        /// <returns>The name of the endpoint.</returns>
        private static string GetEndpointName()
        {
            var type = typeof(T);
            var fullName = type.FullName;
    
            lock (cachedFactories)
            {
                if (cachedEndpointNames.ContainsKey(type))
                {
                    return cachedEndpointNames[type];
                }
    
                var serviceModel = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None).SectionGroups["system.serviceModel"] as ServiceModelSectionGroup;
    
                if ((serviceModel != null) && !string.IsNullOrEmpty(fullName))
                {
                    foreach (var endpointName in serviceModel.Client.Endpoints.Cast<ChannelEndpointElement>().Where(endpoint => fullName.EndsWith(endpoint.Contract)).Select(endpoint => endpoint.Name))
                    {
                        cachedEndpointNames.Add(type, endpointName);
                        return endpointName;
                    }
                }
            }
    
            throw new InvalidOperationException("Could not find endpoint element for type '" + fullName + "' in the ServiceModel client configuration section. This might be because no configuration file was found for your application, or because no endpoint element matching this name could be found in the client element.");
        }
    }
    
    0 讨论(0)
  • 2020-11-22 00:19

    I have written a simple base class that handles this. It's available as a NuGet package and it's quite easy to use.

    //MemberServiceClient is the class generated by SvcUtil
    public class MemberServiceManager : ServiceClientBase<MemberServiceClient>
    {
        public User GetUser(int userId)
        {
            return PerformServiceOperation(client => client.GetUser(userId));
        }
    
        //you can also check if any error occured if you can't throw exceptions       
        public bool TryGetUser(int userId, out User user)
        {
            return TryPerformServiceOperation(c => c.GetUser(userId), out user);
        }
    }
    
    0 讨论(0)
  • 2020-11-22 00:19

    I'd like to add implementation of Service from Marc Gravell's answer for case of using ServiceClient instead of ChannelFactory.

    public interface IServiceConnector<out TServiceInterface>
    {
        void Connect(Action<TServiceInterface> clientUsage);
        TResult Connect<TResult>(Func<TServiceInterface, TResult> channelUsage);
    }
    
    internal class ServiceConnector<TService, TServiceInterface> : IServiceConnector<TServiceInterface>
        where TServiceInterface : class where TService : ClientBase<TServiceInterface>, TServiceInterface, new()
    {
        public TResult Connect<TResult>(Func<TServiceInterface, TResult> channelUsage)
        {
            var result = default(TResult);
            Connect(channel =>
            {
                result = channelUsage(channel);
            });
            return result;
        }
    
        public void Connect(Action<TServiceInterface> clientUsage)
        {
            if (clientUsage == null)
            {
                throw new ArgumentNullException("clientUsage");
            }
            var isChanneldClosed = false;
            var client = new TService();
            try
            {
                clientUsage(client);
                client.Close();
                isChanneldClosed = true;
            }
            finally
            {
                if (!isChanneldClosed)
                {
                    client.Abort();
                }
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-22 00:19

    Our system architecture often uses the Unity IoC framework to create instances of ClientBase so there's no sure way to enforce that the other developers even use using{} blocks. In order to make it as fool-proof as possible, I made this custom class that extends ClientBase, and handles closing down the channel on dispose, or on finalize in case someone doesn't explicitly dispose of the Unity created instance.

    There is also stuff that needed to be done in the constructor to set up the channel for custom credentials and stuff, so that's in here too...

    public abstract class PFServer2ServerClientBase<TChannel> : ClientBase<TChannel>, IDisposable where TChannel : class
    {
        private bool disposed = false;
    
        public PFServer2ServerClientBase()
        {
            // Copy information from custom identity into credentials, and other channel setup...
        }
    
        ~PFServer2ServerClientBase()
        {
            this.Dispose(false);
        }
    
        void IDisposable.Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }
    
        public void Dispose(bool disposing)
        {
            if (!this.disposed)
            {
                try
                {
                        if (this.State == CommunicationState.Opened)
                            this.Close();
                }
                finally
                {
                    if (this.State == CommunicationState.Faulted)
                        this.Abort();
                }
                this.disposed = true;
            }
        }
    }
    

    Then a client can simply:

    internal class TestClient : PFServer2ServerClientBase<ITest>, ITest
    {
        public string TestMethod(int value)
        {
            return base.Channel.TestMethod(value);
        }
    }
    

    And the caller can do any of these:

    public SomeClass
    {
        [Dependency]
        public ITest test { get; set; }
    
        // Not the best, but should still work due to finalizer.
        public string Method1(int value)
        {
            return this.test.TestMethod(value);
        }
    
        // The good way to do it
        public string Method2(int value)
        {
            using(ITest t = unityContainer.Resolve<ITest>())
            {
                return t.TestMethod(value);
            }
        }
    }
    
    0 讨论(0)
提交回复
热议问题