WCFの集約例外と操作記録

# 概要

- WCFサービスを利用した際に集約例外と操作記録を行う方法
- 実務ではDebug.Printの代わりにログツール（NLog, log4net）を利用する

# 環境

- .NETFx 4.8
- Visual Studio 202


# 処理概要

- (1) 集約例外、操作記録の属性`LogErrorOperationAttribute`を作成する
    - `System.Attribute`を継承する
    - `System.Web.Services.Description.IServiceBehavior`インタフェース実装を作成する

- (2) 集約例外
    - `System.Web.Services.Description.IServiceBehavior`インタフェースの`ApplyDispatchBehavior`メソッドで、`serviceHostBase.ChannelDispatchers`のエラーハンドルを利用する
        - 独自に作成した`System.ServiceModel.Dispatcher.IErrorHandler`を継承した`LogErrorHandler`クラスを追加し、この中の`HandleError`イベントで記録する

- (3) 操作記録
    - `System.Web.Services.Description.IServiceBehavior`インタフェースの`ApplyDispatchBehavior`メソッドで、`serviceDescription.Endpoints`の操作`Contract.Operations`に`LoggingOperationBehavior`を設定する
        - `System.Web.Services.Description.IOperationBehavior`実装の`LoggingOperationBehavior`では、`ApplyDispatchBehavior`メソッドで、パラメーターを受け取るためのユーザー定義メソッドとして`LoggingOperationInvoker`を設定する
            - `System.ServiceModel.Dispatcher.IOperationBehavior`実装の`LoggingOperationInvoker`では、`Invoke`メソッドで操作を記録する

- (4) WCFサービスの実装に`LogErrorOperationAttribute`属性を設定する

## 呼び出し側

```vb
Dim sv = New Service1Service.Service1Client
Debug.Print(sv.DoWork("test"))
```

## Web側

### WCFメソッド

- WCFの実装クラスに作成した属性`LogErrorOperationAttribute`を設定する

```vb
' メモ: コンテキスト メニューの [名前の変更] コマンドを使用すると、コード、svc、および config ファイルで同時にクラス名 "Service1" を変更できます。
' 注意: このサービスをテストするために WCF テスト クライアントを起動するには、ソリューション エクスプローラーで Service1.svc または Service1.svc.vb を選択し、デバッグを開始してください。
<LogErrorOperationAttribute>
Public Class Service1
    Implements IService1

    Public Function DoWork(value1 As String) As String Implements IService1.DoWork
        Return $"{value1}:{Now}"
    End Function
End Class
```

### ログ

```
開始 アクション名:IService1/DoWork,引数:"p1":"test",
終了 アクション名:IService1/DoWork
```


### 属性

- 操作記録は簡易的な実装

```vb:LogErrorOperationAttribute.vb
Imports System.Collections.ObjectModel
Imports System.Net
Imports System.ServiceModel
Imports System.ServiceModel.Channels
Imports System.ServiceModel.Description
Imports System.ServiceModel.Dispatcher
Imports System.Web.Services.Description

''' <summary>
''' 集約例外、操作記録のための拡張
''' WCFサービスの属性として設定
''' </summary>
Public Class LogErrorOperationAttribute
    Inherits Attribute
    Implements IServiceBehavior

    Private _errorHandler As LogErrorHandler

    Public Sub New()
        _errorHandler = New LogErrorHandler
    End Sub

    Public Sub Validate(serviceDescription As Description.ServiceDescription, serviceHostBase As ServiceHostBase) Implements IServiceBehavior.Validate
    End Sub

    Public Sub AddBindingParameters(serviceDescription As Description.ServiceDescription, serviceHostBase As ServiceHostBase, endpoints As Collection(Of ServiceEndpoint), bindingParameters As BindingParameterCollection) Implements IServiceBehavior.AddBindingParameters
    End Sub

    Public Sub ApplyDispatchBehavior(serviceDescription As Description.ServiceDescription, serviceHostBase As ServiceHostBase) Implements IServiceBehavior.ApplyDispatchBehavior
        '集約例外
        For Each channelDispatcherBase In serviceHostBase.ChannelDispatchers
            Dim channelDispatcher = CType(channelDispatcherBase, ChannelDispatcher)
            If channelDispatcher IsNot Nothing Then
                channelDispatcher.ErrorHandlers.Add(_errorHandler)
            End If
        Next

        '操作記録
        For Each endpoint In serviceDescription.Endpoints
            For Each operation In endpoint.Contract.Operations
                Dim behavior = New LoggingOperationBehavior()
                operation.Behaviors.Add(behavior)
            Next
        Next
    End Sub
End Class

''' <summary>
''' 集約例外
''' </summary>
Public Class LogErrorHandler
    Implements IErrorHandler

    Public Sub ProvideFault([error] As Exception, version As MessageVersion, ByRef fault As Channels.Message) Implements IErrorHandler.ProvideFault
    End Sub

    Public Function HandleError([error] As Exception) As Boolean Implements IErrorHandler.HandleError
        Debug.Print([error].ToString)
        Return True
    End Function
End Class

''' <summary>
''' 操作記録
''' </summary>
Public Class LoggingOperationBehavior
    Implements IOperationBehavior

    Public Sub Validate(operationDescription As OperationDescription) Implements IOperationBehavior.Validate
    End Sub

    Public Sub ApplyDispatchBehavior(operationDescription As OperationDescription, dispatchOperation As DispatchOperation) Implements IOperationBehavior.ApplyDispatchBehavior
        dispatchOperation.Invoker = New LoggingOperationInvoker(dispatchOperation.Invoker, dispatchOperation)
    End Sub

    Public Sub ApplyClientBehavior(operationDescription As OperationDescription, clientOperation As ClientOperation) Implements IOperationBehavior.ApplyClientBehavior
    End Sub

    Public Sub AddBindingParameters(operationDescription As OperationDescription, bindingParameters As BindingParameterCollection) Implements IOperationBehavior.AddBindingParameters
    End Sub
End Class

''' <summary>
''' 操作記録
''' </summary>
Public Class LoggingOperationInvoker
    Implements IOperationInvoker

    Private _baseInvoker As IOperationInvoker

    ''' <summary>
    ''' 操作のアクション。例：http://tempuri.org/IService1/DoWork
    ''' </summary>
    Private _action As String
    Public Sub New(baseInvoker As IOperationInvoker, operation As DispatchOperation)
        _baseInvoker = baseInvoker
        _action = operation.Action?.Replace("http://tempuri.org/", "")
    End Sub

    Public ReadOnly Property IsSynchronous As Boolean Implements IOperationInvoker.IsSynchronous
        Get
            Return _baseInvoker.IsSynchronous
        End Get
    End Property

    Public Function AllocateInputs() As Object() Implements IOperationInvoker.AllocateInputs
        Return _baseInvoker.AllocateInputs()
    End Function

    Public Function Invoke(instance As Object, inputs() As Object, ByRef outputs() As Object) As Object Implements IOperationInvoker.Invoke
        Try
            Dim argString = GetArgString(inputs)
            Debug.Print($"開始 アクション名:{_action},引数:{argString}")
        Catch ex As Exception
            Debug.Print(ex.ToString())
            Debug.Print($"開始 LOGGING_PARAMETER_EXCEPTION")
        End Try

        Dim result = _baseInvoker.Invoke(instance, inputs, outputs)

        Debug.Print($"終了 アクション名:{_action}")

        Return result
    End Function

    Private Function GetArgString(inputs() As Object) As String
        Dim i = 0

        Dim sb = New StringBuilder()
        For Each item As Object In inputs
            i += 1
            sb.Append($"""p{i}"":")
            If (TypeOf (item) Is String) OrElse (TypeOf (item) Is DateTime) Then
                sb.Append($"""{item}""")
            ElseIf item Is Nothing Then
                sb.Append($"Nothing")
            Else
                Dim aryPublicProperties = item.GetType()?.GetProperties()

                If aryPublicProperties Is Nothing Then
                    sb.Append($"""{item}""")
                Else
                    sb.Append($"""{item}"":{{")
                    For Each p In aryPublicProperties
                        sb.Append($"""{p.Name}"":""{p.GetValue(item, Nothing)}"",")
                    Next
                End If
            End If
            sb.Append(",")
        Next

        Return sb.ToString()
    End Function

    Public Function InvokeBegin(instance As Object, inputs() As Object, callback As AsyncCallback, state As Object) As IAsyncResult Implements IOperationInvoker.InvokeBegin
        Return _baseInvoker.InvokeBegin(instance, inputs, callback, state)
    End Function

    Public Function InvokeEnd(instance As Object, ByRef outputs() As Object, result As IAsyncResult) As Object Implements IOperationInvoker.InvokeEnd
        Return _baseInvoker.InvokeEnd(instance, outputs, result)
    End Function
End Class
```

# 参考

https://learn.microsoft.com/ja-jp/dotnet/api/system.servicemodel.description.iservicebehavior?view=netframework-4.8

https://tnakamura.hatenablog.com/entry/20090904/error_handler

https://stackoverflow.com/questions/36419810/iservicebehavior-prevent-applying-message-inspectors-for-some-messages