为 wcf webhttpbinding 提供例外

Provide exceptions for wcf webhttpbinding

提问人:user1069516 提问时间:2/12/2020 更新时间:2/18/2020 访问量:196

问:

我必须将 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 异常 WebHttpBinding

评论


答:

0赞 Abraham Qian 2/14/2020 #1

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

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

请参阅此链接。
https://learn.microsoft.com/en-us/windows/win32/http/add-sslcert
这是一个相关的讨论。
如何禁用对 Windows 服务中托管的 WCF 的 HTTPS 调用的凭据输入此外,我没有看到您将 应用于自承载服务
。这通常由服务终结点行为实现。
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;
        }
}

结果。
enter image description here
如果有什么我可以帮忙的,请随时告诉我。

评论

0赞 user1069516 2/14/2020
谢谢你的回答。Global 在业务类中使用,如下所示: [GlobalErrorBehavior(typeof(GlobalErrorHandler))] public class BSUsers : IBFUsers
0赞 user1069516 2/14/2020
谢谢你的回答。GlobaErrorHandler 用于在类定义之前具有属性的业务类。如果我使用 Web 浏览器调用服务,则会正确捕获错误消息和 http 状态代码,但不在 Exception 对象的单元测试中,如果我返回 404 结果,我只是“未找到”
0赞 Abraham Qian 2/17/2020
问题应该出在服务的调用方式上。通常,我们不使用代理来调用 webHttpbinding(REST API) 创建的服务,而是使用 HttpClient 类库直接发送 Get/Post 请求。我同样可以使用Postman捕获自定义错误。如果我们通过代理或通道工厂调用服务,我们必须确保客户端的绑定与服务器一致,服务合约也是如此。然后在服务终结点上添加 webhttpbehavior。
0赞 Abraham Qian 2/17/2020
下面是使用代理调用 Rest WCF 服务的示例。stackoverflow.com/questions/54018130/......
0赞 user1069516 2/18/2020 #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。

谢谢你的发言。