【问题标题】:Provide exceptions for wcf webhttpbinding为 wcf webhttpbinding 提供异常
【发布时间】:2020-05-28 02:55:40
【问题描述】:

我必须将 wcf web 服务的绑定从 tcpbinding 更改为带有基本身份验证和 ssl 的 webhttpbinding。

Web 服务在控制台应用程序和生产版本的 Windows 服务中自托管。一些本地服务使用命名管道绑定,就像一个服务调用另一个服务一样。

一切正常,但不是全局错误管理器(实现 IErrorHandler 接口的类)

某些 DAL 或业务方法会抛出带有自定义消息的异常,并且此消息已正确提供给客户端(一段时间的单元测试)。但是由于我更改了绑定,单元测试中捕获的异常始终是 500 错误,内部服务器错误和自定义消息不在异常对象中。

服务器代码:

// Création de l'URI
var baseAddress = new Uri($"https://localhost/blablabla/{typeof(TBusiness).Name}");

// Création du Host avec le type de la classe Business
var host = new ServiceHost(typeof(TBusiness), baseAddress);

// Liaison WebHttpBinding sécurité transport
var binding = new WebHttpBinding
{
   MaxBufferSize = 2147483647,
   MaxReceivedMessageSize = 2147483647,
   Security = new WebHttpSecurity
   {
       Mode = WebHttpSecurityMode.Transport
   },
};

binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;

// Permet de renvoyer du xml et du json
var webBehavior = new WebHttpBehavior
{
   AutomaticFormatSelectionEnabled = true
};

var ep = host.AddServiceEndpoint(typeof(TContracts), binding, "");
ep.Behaviors.Add(webBehavior);

var sdb = host.Description.Behaviors.Find<ServiceDebugBehavior>();
sdb.HttpHelpPageEnabled = false;

// Activation https
var smb = new ServiceMetadataBehavior
{
   HttpGetEnabled = false,
   HttpsGetEnabled = true,
};

host.Description.Behaviors.Add(smb);

// Ajout de l'authentification
var customAuthenticationBehavior = new ServiceCredentials();
customAuthenticationBehavior.UserNameAuthentication.UserNamePasswordValidationMode = UserNamePasswordValidationMode.Custom;
customAuthenticationBehavior.UserNameAuthentication.CustomUserNamePasswordValidator = new SessionAuthentication();
host.Description.Behaviors.Add(customAuthenticationBehavior);

// Démarrage du host
host.Open();

抛出异常的业务方法:

public TOUser GetUserByLogin(string login)
{
  using (var service = new ServiceProviderNamedPipe<IBFSessionManager, BSSessionManager>())
  {
     // Récupération de la DALUsers
     var dal = service.Channel.GetDALUsers(OperationContext.Current.ServiceSecurityContext.PrimaryIdentity.Name);
     var user = dal.GetUserByLogin(login);

     if (user == null) throw new FaultException(Errors.DALUsers_Err001);

     return BMToolsEntitiesToTO.UserToTOUser(user);
   }
}

错误全局管理器:

public class GlobalErrorHandler : IErrorHandler
{
    public bool HandleError(Exception error)
    {
        // Empèche la propagation de l'erreur
        return true;
    }

    public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
    {
        var msg = error.Message;

        // Création de l'exception de retour
        var newEx = new FaultException(msg);
        var msgFault = newEx.CreateMessageFault();
        fault = Message.CreateMessage(version, msgFault, newEx.Action);
    }
}

单元测试:

public void GetUserByLoginWithUnknownLoginTest()
{
    TOUser user = null;
    using (var service = new ServiceProviderHTTP<IBFUsers, BSUsers>(_user))
    {
        try
        {
            user = service.Channel.GetUserByLogin("1234");
        }
        catch (Exception e)
        {
            // e.message always provide "Internal server error instead of custom message (Errors.DALUsers_Err001)
            Assert.AreEqual(Errors.DALUsers_Err001, e.Message);
        }

        Assert.IsNull(user);
    }
}

自从我更改绑定后,所有捕获异常的单元测试都失败了。

感谢您的帮助。

【问题讨论】:

    标签: wcf exception webhttpbinding


    【解决方案1】:

    我怀疑您的服务是否正常运行。由于传输层安全性(使用 HTTPS),您是否将证书绑定到默认端口 443?请使用以下语句将证书绑定到 443 端口。

    netsh http add sslcert ipport=0.0.0.0:443 certhash=c20ed305ea705cc4e36b317af6ce35dc03cfb83d appid={c9670020-5288-47ea-70b3-5a13da258012}
    

    请参考此链接。
    https://docs.microsoft.com/en-us/windows/win32/http/add-sslcert
    这是一个相关的讨论。
    How to disable credentials input for HTTPS call to my WCF hosted in windows service
    此外,我没有看到您将GlobalErrorHandler 应用于自托管服务。这通常由服务端点行为实现。

    ServiceEndpoint se = sh.AddServiceEndpoint(typeof(IService),new WebHttpBinding(), "");
                    MyEndpointBehavior bhv = new MyEndpointBehavior();
                    se.EndpointBehaviors.Add(bhv);
    

    我写了一个例子,希望对你有用。

    class Program
        {
            static void Main(string[] args)
            {
                //I have already bound a certificate to the 21011 port.
                var baseAddress = new Uri($"https://localhost:21011");
                var host = new ServiceHost(typeof(MyService), baseAddress);
    
                var binding = new WebHttpBinding
                {
                    MaxBufferSize = 2147483647,
                    MaxReceivedMessageSize = 2147483647,
                    Security = new WebHttpSecurity
                    {
                        Mode = WebHttpSecurityMode.Transport
                    },
                };
                //basic authentication use windows login account located on the server-side instead of the below configuration(UserNamePasswordValidationMode.Custom)
                binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;
    
                // Permet de renvoyer du xml et du json
                var webBehavior = new WebHttpBehavior
                {
                    AutomaticFormatSelectionEnabled=true
                };
    
                var ep = host.AddServiceEndpoint(typeof(IService), binding, "");
                ep.Behaviors.Add(webBehavior);
                MyEndpointBehavior bhv = new MyEndpointBehavior();
                ep.EndpointBehaviors.Add(bhv);
    
                var sdb = host.Description.Behaviors.Find<ServiceDebugBehavior>();
                sdb.HttpHelpPageEnabled = false;
    
                // Activation https
                var smb = new ServiceMetadataBehavior
                {
                    HttpGetEnabled = true,
                    HttpsGetEnabled = true,
                };
    
                host.Description.Behaviors.Add(smb);
    
                // Ajout de l'authentification
                //var customAuthenticationBehavior = new ServiceCredentials();
                //customAuthenticationBehavior.UserNameAuthentication.UserNamePasswordValidationMode = UserNamePasswordValidationMode.Custom;
                //customAuthenticationBehavior.UserNameAuthentication.CustomUserNamePasswordValidator = new SessionAuthentication();
                //host.Description.Behaviors.Add(customAuthenticationBehavior);
    
                // Démarrage du host
                host.Open();
                Console.WriteLine("service is running....");
                Console.ReadLine();
    
                Console.WriteLine("Closing.....");
                host.Close();
    
            }
        }
    
    
        [ServiceContract(ConfigurationName = "isv")]
        public interface IService
        {
            [OperationContract]
            [WebGet]
            string Delete(int value);
        }
        [ServiceBehavior(ConfigurationName = "sv")]
        public class MyService : IService
        {
            public string Delete(int value)
            {
                if (value <= 0)
                {
                    throw new ArgumentException("Parameter should be greater than 0");
                }
                return "Hello";
            }
    
        }
        public class MyError
        {
            public string Details { get; set; }
            public string Error { get; set; }
    
        }
        public class MyCustomErrorHandler : IErrorHandler
        {
            public bool HandleError(Exception error)
            {
                return true;
            }
    
            public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
            {
                MyError myerror = new MyError()
                {
                    Details = error.Message,
                    Error = "An error occured"
                };
    
                fault = Message.CreateMessage(version, "messsagefault", myerror);
            }
        }
        public class MyEndpointBehavior : IEndpointBehavior
        {
            public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
            {
                return;
            }
    
            public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
            {
                return;
            }
    
            public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
            {
                MyCustomErrorHandler myCustomErrorHandler = new MyCustomErrorHandler();
                endpointDispatcher.ChannelDispatcher.ErrorHandlers.Add(myCustomErrorHandler);
            }
    
            public void Validate(ServiceEndpoint endpoint)
            {
                return;
            }
    }
    

    结果。

    如果有什么可以帮助的,请随时告诉我。

    【讨论】:

    • 感谢您的回答。 Global 在 Business 类中使用如下:[GlobalErrorBehavior(typeof(GlobalErrorHandler))] public class BSUsers : IBFUsers
    • 感谢您的回答。 GlobaErrorHandler 用于在类定义之前具有属性的业务类。如果我使用网络浏览器调用服务,则错误消息和 http 状态代码被正确捕获,但不是在 Exception 对象的单元测试中,如果我返回 404 结果,我只是“未找到”
    • 问题应该是服务的调用方式。一般我们不使用代理调用webHttpbinding(REST API)创建的服务,而是使用HttpClient类库直接发送Get/Post请求。我同样可以使用 Postman 捕获自定义错误。如果我们通过代理或者通道工厂调用服务,必须保证客户端的绑定与服务端一致,Service Contract也是如此。然后在服务端点上添加 webhttpbehavior。
    • 这里是一个使用代理调用Rest WCF服务的例子。 stackoverflow.com/questions/54018130/…
    【解决方案2】:

    经过多次搜索,我发现很多人都有同样的问题。

    这是我的解决方案:

    在服务器端,始终使用正确的 HTTP 状态代码抛出这样的 WebFaultException:

    throw new WebFaultException<string>(myStringMessage, HttpStatusCode.NotFound);
    

    在客户端(仅用于单元测试或 MVC 项目),将异常转换为调用 Response 对象上的 GetResponseStream 以获取自定义消息:

    var err = (WebException)e;
    using (Stream respStream = err.Response.GetResponseStream())
    {
        using (var reader = new StreamReader(respStream))
        {
            var serializer = new XmlSerializer(typeof(string));
            var response = reader.ReadToEnd();
            return response.Substring(response.IndexOf('>') + 1).Replace("</string>", "");
        }
    }
    

    在 IErrorHandler 的 ProvideFault 方法中,我只是添加代码以在文件中写入错误,而不是使用 Message.CreateMessage 方法创建消息。

    它可以正常工作,但是在 ProvideFault 之后会生成一个 EndPointNotFoundException,在其他一些帖子中我看到可能会抛出一个 ProtocolException。

    感谢您的发言。

    【讨论】:

      猜你喜欢
      • 2010-12-14
      • 2012-12-25
      • 1970-01-01
      • 1970-01-01
      • 2014-03-10
      • 2011-07-14
      • 1970-01-01
      • 1970-01-01
      • 2010-12-03
      相关资源
      最近更新 更多