Webサービスで呼び出されたWebメソッドを自動で記録する方法

# 目的

Webサービスで呼び出されたWebメソッドを自動で記録する必要があり、後付けで行わなければならない場合の簡易的な方法について記載します。

# 概要

``SoapExtension``を継承したクラスを作成し、``ProcessMessage``関数で``System.Web.Services.Protocols.SoapMessage.Stage``が``SoapMessageStage.AfterSerialize``の場合に処理を行うようにします。

さらに、Web.Configで作成したクラスが、自動で呼び出されるように設定します。


# 実装

## ``SoapExtension``を継承したクラス

```vb:CustomSoapExtension.vb
Public Class CustomSoapExtension
    Inherits SoapExtension

    Public Overrides Function GetInitializer(ByVal serviceType As System.Type) As Object
        Return Nothing
    End Function

    Public Overrides Function GetInitializer(ByVal methodInfo As System.Web.Services.Protocols.LogicalMethodInfo, ByVal attribute As System.Web.Services.Protocols.SoapExtensionAttribute) As Object
        Return Nothing
    End Function

    Public Overrides Sub Initialize(ByVal initializer As Object)
    End Sub

    Public Overrides Sub ProcessMessage(ByVal message As System.Web.Services.Protocols.SoapMessage)
        Select Case message.Stage
            Case SoapMessageStage.BeforeDeserialize
            Case SoapMessageStage.AfterDeserialize
                '関数をを記録する場合はここで行う。
                LogInfoForWebServicesStart()
            Case SoapMessageStage.BeforeSerialize
            Case SoapMessageStage.AfterSerialize

                If message.Exception IsNot Nothing Then
                    Dim mesEx As Exception = message.Exception
                    'エラーを記録する場合はここで行う。
                End If
            Case Else
                Throw New ApplicationException
        End Select
    End Sub
    
    ''' <summary>
    ''' Webサービスメソッドのスタート時にログ情報を出力する
    ''' </summary>
    Private Sub LogInfoForWebServicesStart()
        If System.Web.HttpContext.Current Is Nothing Then
            Return
        End If

        Dim message As String = GetWebServiceInfos()

        'ログを記録する（log4Netなど）
        （log4Netなどを使用する）
    End Sub
    
    ''' <summary>
    ''' メソッド名などの情報を取得
    ''' </summary>
    Private Function GetWebServiceInfos() As String
        '' ガード条件
        If Web.HttpContext.Current Is Nothing Then Return Nothing

        Dim strInfo As New System.Text.StringBuilder
        strInfo.Length = 0

        Dim request As Web.HttpRequest = Web.HttpContext.Current.Request
        If request IsNot Nothing Then
            'URLを取得
            Dim queryKEYOfURL = From el In request.ServerVariables.AllKeys
                                Where el?.ToString = "URL"

            If queryKEYOfURL Is Nothing Then Return Nothing
            If queryKEYOfURL.Count = 0 Then Return Nothing

            Dim url = ""
            url = request.ServerVariables(queryKEYOfURL.First)

            Dim ip = request.UserHostAddress
            Dim methodName = GetMethodName()

            strInfo.Append($"IP={ip}")
            strInfo.Append($",URL={url}/{methodName}")
        End If

        Return strInfo.ToString
    End Function

    ''' <summary>
    ''' Webメソッド名の取得
    ''' </summary>
    ''' <remarks>
    ''' サンプル
    '''<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
    '''   <s:Body>
    '''	    <TryLogin xmlns = "http://tempuri.org/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
    '''		    <userid>00020001</userid>
    '''	    </TryLogin>
    '''   </s:Body>
    '''</s:Envelope>
    ''' </remarks>
    Private Function GetMethodName() As String
        Dim request As Web.HttpRequest = Web.HttpContext.Current.Request
        If request.ServerVariables("HTTP_SOAPACTION") Is Nothing Then Return　Nothing

        Dim savedPosition As Long = request.InputStream.Position
        Try
            Dim inputStream(request.ContentLength) As Byte
            Dim strSOAP As New Text.StringBuilder

            '読み込み
            request.InputStream.Position = 0
            request.InputStream.Read(inputStream, 0, request.ContentLength)
            strSOAP.Length = 0
            strSOAP.Append(request.ContentEncoding.GetString(inputStream))
            'NullCharを削除
            strSOAP = strSOAP.Replace(vbNullChar, "")

            Dim xDocument As XmlDocument = New XmlDocument
            Call xDocument.Load(New IO.StringReader(strSOAP.ToString)) 'XMLファイルをロード   

            Dim xRoot = xDocument.DocumentElement 'XMLドキュメントからルート要素を取り出す  

            Dim xDataList = xRoot.GetElementsByTagName("soap:Body") 'ルート要素から親リストを取得する  
            If xDataList?.Count = 0 Then
                xDataList = xRoot.GetElementsByTagName("s:Body") 'ルート要素から親リストを取得する  
            End If

            Return GetMethodNameSub1(xDataList)
        Finally
            '読出しのために移動した位置をもとに戻す
            request.InputStream.Position = savedPosition
        End Try
    End Sub

    Private Function GetMethodNameSub1(xDataList As XmlNodeList) As String
        If xDataList Is Nothing Then Return Nothing
        If xDataList.Count = 0 Then Return Nothing

        Return GetMethodNameSub2(xDataList(0).ChildNodes)
    End Sub

    Private Function GetMethodNameSub2(node As XmlNodeList) As String
        If node Is Nothing Then Return Nothing

        'XML Elementのみ取得
        Dim query = From el In node
                    Where TypeOf (el) Is XmlElement
                    Select xmlEl = CType(el, XmlElement)

        Return query.First?.Name
    End Sub
End Class
```

## Web.Configで作成したクラスを呼び出すための設定

```config:web.config
  <system.web>
    <webServices>
      <soapExtensionTypes>
        <add type="(名前空間).CustomSoapExtension, (アセンブリ名)"/>
      </soapExtensionTypes>
    </webServices>
  </system.web>
```