是否可以使用 IErrorHandler 返回与合约中预期匹配的响应 DTO

本文关键字:DTO 响应 可以使 IErrorHandler 返回 是否 | 更新日期: 2023-09-27 18:31:40

我希望能够捕获所有未经处理的异常并返回预期的 DTO,但填写了一些错误信息。例如

public class CreateFooRequest
{
    public string Name { get; set; }
}
public class CreateFooResponse
{
    public Foo Created { get; set; }
    public string Error { get; set; }  // If call was successful then this will be null
    public string Detail { get; set; }
}
public interface IFooService
{
    CreateFooResponse Create(CreateFooRequest request);
}
public ErrorHandler: IErrorHandler
{
    public bool Handle(Exception ex)
    {
        return true;
    }
    public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
    {
        // Some how figure out that IFooService.Create was called. 
        // Inspect the method signature and see that there is an input called CreateFooRequest
        // Use reflection to initialize response objects that will replace the "Request" with "Response"
        var response = new CreateFooResponse();
        response.Error = error.GetType().Name;
        // I think i need one of the following overloads
        fault = Message.CreateMessage(version, action, response);
    }
}

做这样的事情可能吗?我们使用NetTCP作为我们的绑定,如果这有所作为的话。

是否可以使用 IErrorHandler 返回与合约中预期匹配的响应 DTO

IErrorHandler 旨在生成错误合约。如果您不想返回错误,最好采用拦截器方法并使用 IOperationInvoker 扩展点。

操作调用程序是实际调用服务方法的 WCF 框架的一部分。扩展它时,可以有效地"拦截"对服务的调用。请注意,调用程序确实有责任。不能简单地替换 WCF 调用程序实现。相反,你把它们链接起来(见下文)。

概括地说,步骤是:

  1. 创建一个实现try/catch块的IOperationInvoker.Invoke()。捕获您希望成为响应消息而不是故障异常的异常。
  2. 创建 IOperationBehavior(也可以选择也是一个属性)以将该行为应用于服务。

该方法有几个优点:

  1. 在 WCF 看到异常之前捕获该异常。
  2. 在 IOperationBehavior.ApplyDispatchBehavior() 中,您可以在服务启动时访问操作描述。如果将其保存在调用程序中,则无需使用反射来捕获方法的返回类型。
  3. IOperationBehavior.Validate() 允许可靠的检查,以确保返回类型可以实际处理。

下面是演示该方法的完整 Windows 控制台应用程序。将其粘贴到 Visual Studio 中,添加明显的程序集引用,然后运行它。

对于大量的代码和基类的过度使用,我深表歉意。我正在减少实际的生产代码。

如果你想更好地了解IOperationInvoker扩展点以及InvokerBase类的示例正在做什么,请参阅Carlos Figueira的博客。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.Text;
using System.Threading.Tasks;
namespace WcfErrorResponse
{
    /// <summary>
    /// Provides a base IOperationInvoker implementation that stores and passes through calls to the exisiting (old) invoker
    /// </summary>
    public abstract class InvokerBase : IOperationInvoker
    {
        private readonly IOperationInvoker m_OldInvoker;
        protected IOperationInvoker OldInvoker
        {
            get { return m_OldInvoker; }
        }
        public InvokerBase(IOperationInvoker oldInvoker)
        {
            m_OldInvoker = oldInvoker;
        }
        public virtual object[] AllocateInputs()
        {
            return OldInvoker.AllocateInputs();
        }
        public virtual object Invoke(object instance, object[] inputs, out object[] outputs)
        {
            return OldInvoker.Invoke(instance, inputs, out outputs);
        }
        public virtual IAsyncResult InvokeBegin(object instance, object[] inputs, AsyncCallback callback, object state)
        {
            return OldInvoker.InvokeBegin(instance, inputs, callback, state);
        }
        public virtual object InvokeEnd(object instance, out object[] outputs, IAsyncResult result)
        {
            return OldInvoker.InvokeEnd(instance, out outputs, result);
        }
        public virtual bool IsSynchronous
        {
            get { return OldInvoker.IsSynchronous; }
        }
    }
    /// <summary>
    /// Base implementation for a Method level attribte that applies a <see cref="InvokerBase"/> inherited behavior.
    /// </summary>
    [AttributeUsage(AttributeTargets.Method)]
    public abstract class InvokerOperationBehaviorAttribute : Attribute, IOperationBehavior
    {
        protected abstract InvokerBase CreateInvoker(IOperationInvoker oldInvoker, OperationDescription operationDescription, DispatchOperation dispatchOperation);
        public void AddBindingParameters(OperationDescription operationDescription, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
        { }
        public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
        { }
        public virtual void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
        {
            // chain invokers.
            IOperationInvoker oldInvoker = dispatchOperation.Invoker;
            dispatchOperation.Invoker = CreateInvoker(oldInvoker, operationDescription, dispatchOperation);
        }
        public virtual void Validate(OperationDescription operationDescription)
        {
            return;
        }
    }
    public class ResponseExceptionInvoker : InvokerBase
    {
        private Type returnType;
        public ResponseExceptionInvoker(IOperationInvoker oldInvoker, OperationDescription operationDescription)
            : base(oldInvoker)
        {
            // save the return type for creating response messages
            this.returnType = operationDescription.GetReturnType();
            if (this.returnType == null)
            {
                throw new InvalidOperationException("The operation '" + operationDescription.SyncMethod.DeclaringType.Name + "' does not define a return type.");
            }
        }
        public override object Invoke(object instance, object[] inputs, out object[] outputs)
        {
            object returnedValue = null;
            object[] outputParams = new object[] { };
            outputs = new object[] { };
            try
            {
                returnedValue = OldInvoker.Invoke(instance, inputs, out outputParams);
                outputs = outputParams;
                return returnedValue;
            }
            catch (Exception ex)
            {
                Logger.Debug("ResponseExceptionInvoker() - Caught Exception. A Response Message will be returned. Message='" + ex.Message + "'");
                // there was an excpetion. Do not assign output params... their state is undefined.
                //outputs = outputParams;
                try
                {
                    // assumes the behavior only used for return types that inherit from Response, as verified by ResponseExceptionOperationBehaviorAttribute.Validate()
                    Response response = (Response)Activator.CreateInstance(this.returnType);
                    response.Success = false;
                    response.ErrorMessage = ex.Message;
                    return response;
                }
                catch (Exception exCreateResponse)
                {
                    // Log that the Response couldn't be created and throw the original exception.
                    // Probably preferable to wrap and throw.
                    Logger.Error("Caught ResponseException, but unable to create the Response object. Likely indicates a bug or misconfiguration. Exception will be rethrown." + exCreateResponse.Message);
                }
                throw;
            }
        }
    }
    public class ResponseExceptionOperationBehaviorAttribute : InvokerOperationBehaviorAttribute
    {
        protected override InvokerBase CreateInvoker(IOperationInvoker oldInvoker, OperationDescription operationDescription, DispatchOperation dispatchOperation)
        {
            return new ResponseExceptionInvoker(oldInvoker, operationDescription);
        }
        public override void Validate(OperationDescription operationDescription)
        {
            // validate that this attribute can be applied to the service behavior.
            Type returnType = operationDescription.GetReturnType();
            if (!typeof(Response).IsAssignableFrom(returnType))
            {
                throw new InvalidOperationException("'" + returnType.FullName + "' does not inherit from '" + typeof(Response).FullName +
                                                    "'. ImplicitResponse behavior applied to '" + operationDescription.SyncMethod.DeclaringType.Name + "." + operationDescription.Name +
                                                    "' requires the method return type inherit from '" + typeof(Response).FullName);
            }
        }
    }
    static class OperationDescriptionExtensions
    {
        public static Type GetReturnType(this OperationDescription operationDescription)
        {
            if (operationDescription.SyncMethod == null)
                throw new InvalidOperationException("These behaviors have only been tested with Sychronous methods.");
            // !! Warning: This does NOT work for Asynch or Task based implementations.
            System.Reflection.MethodInfo method = operationDescription.SyncMethod ?? operationDescription.EndMethod;
            return method.ReturnType;
        }
    }
    // When not using FaultContracts, return success/fail as a part of all responses via some base class properties.
    [DataContract]
    public class Response
    {
        [DataMember]
        public bool Success { get; set; }
        [DataMember]
        public string ErrorMessage { get; set; }
    }
    public class ChildResponse : Response
    {
        [DataMember]
        public string Foo { get; set; }
    }
    [DataContract]
    public class Request
    {
        [DataMember]
        public string Name { get; set; }
    }
    [ServiceContract]
    public interface ISimple
    {
        [OperationContract]
        ChildResponse Work(Request request);
        [OperationContract]
        ChildResponse Fail(Request request);
    }
    public class SimpleService : ISimple
    {
        public ChildResponse Work(Request request) {
            return new ChildResponse() { Success = true };
        }
        [ResponseExceptionOperationBehavior]
        public ChildResponse Fail(Request request)
        {
            throw new NotImplementedException("This method isn't done");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            ServiceHost simpleHost = new ServiceHost(typeof(SimpleService), new Uri("http://localhost/Simple"));
            simpleHost.Open();
            ChannelFactory<ISimple> factory = new ChannelFactory<ISimple>(simpleHost.Description.Endpoints[0]);
            ISimple proxy = factory.CreateChannel();
            Logger.Debug("Calling Work...");
            var response1 = proxy.Work(new Request() { Name = "Foo" });
            Logger.Debug("Work() returned Success=" + response1.Success + " message='" + response1.ErrorMessage + "'");
            Logger.Debug("Calling Fail...");
            var response2 = proxy.Fail(new Request() { Name = "FooBar" });
            Logger.Debug("Fail() returned Success=" + response2.Success + " message='" + response2.ErrorMessage + "'");
            Console.WriteLine("Press ENTER to close the host.");
            Console.ReadLine();
            ((ICommunicationObject)proxy).Shutdown();
            simpleHost.Shutdown();
        }
    }

    public static class CommunicationObjectExtensions
    {
        static public void Shutdown(this ICommunicationObject obj)
        {
            try
            {
                obj.Close();
            }
            catch (Exception ex)
            {
                Console.WriteLine("Shutdown exception: {0}", ex.Message);
                obj.Abort();
            }
        }
    }
    public static class Logger
    {
        public static void Debug(string message) { Console.WriteLine(message); }
        public static void Error(string message) { Console.WriteLine(message); }
    }
}

在您的位置,我将通过使用通用try/catch块尽可能简单地实现这一点。使用自定义异常处理程序来观察调用的服务方法并使用反射创建相应的响应对我来说似乎有点过分。

让您的生活更轻松:

public CreateFooResponse Create(CreateFooRequest request)
{
    try
    {
        // Create Foo
        var foo = CreateFoo();
        // Return successful CreateFooResponse
        return new CreateFooResponse
        {
            Created = foo,
            Error = null,
            Detail = "Created successfully"
        };
    }
    catch (Exception ex)
    {
        // Return CreateFooResponse with an error
        return new CreateFooResponse
        {
            Created = null,
            Error = CreateError(ex),
            Detail = "Unable to create Foo."
        };
    }
}

使用自定义 WCF 错误处理程序的一个很好的例子是记录错误,将其转换为FaultContract并返回到调用方。你有不同的情况,我会建议一种不同的方法。

首先,我想说的是,使用错误合约要好得多。下面我将给出一个服务 GetDataUsingDataContract 的示例:

[OperationContract]
CompositeType GetDataUsingDataContract(CompositeType composite);
[DataContract]
public class CompositeType 
{
    [DataMember]
    public bool BoolValue { get; set; }
    [DataMember]
    public string StringValue { get; set; }
}

然后,创建一个等效于正常响应的 bodyWriter:

public class MyBodyWriter : BodyWriter
{
    public CompositeType CompositeType { get; private set; }
    public MyBodyWriter(CompositeType composite)
        : base(false)
    {
        CompositeType = composite;
    }
    protected override void OnWriteBodyContents(XmlDictionaryWriter writer)
    {
       writer.WriteStartElement("GetDataUsingDataContractResponse", "http://tempuri.org/");
       writer.WriteStartElement("GetDataUsingDataContractResult");
       writer.WriteAttributeString("xmlns", "a", null, "http://schemas.datacontract.org/2004/07/WcfService1");
       writer.WriteAttributeString("xmlns", "i", null, "http://www.w3.org/2001/XMLSchema-instance");
       writer.WriteStartElement("a:BoolValue");
       writer.WriteString(CompositeType.BoolValue.ToString().ToLower());
       writer.WriteEndElement();
       writer.WriteStartElement("a:StringValue");
       writer.WriteString(CompositeType.StringValue);
       writer.WriteEndElement();
       writer.WriteEndElement();
    }
}

最后,你在IErrorHandler中使用它:

    public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
    {
        // TODO: parse error and gets response
        var response = new CompositeType {BoolValue = true, StringValue = "a"};
        fault = Message.CreateMessage(version, "http://tempuri.org/", new MyBodyWriter(response));
    }

我做了一个测试,如果服务引发异常或服务正常响应,我可以得到正确的答案:

    var response1 = client.GetDataUsingDataContract(null);
    var response2 = client.GetDataUsingDataContract(new CompositeType { StringValue = "a", BoolValue = true });