VB.Netでカスタム属性を作成

# 背景
プロパティのまとまったクラスを使用し、CSV出力をしたいが、
プロパティ名を全て記載してCSV出力のためのファイルを作るのは手間がかかるし、
1つ追加する度にソースの変更が発生してしまう。
ということで、プロパティに順番のカスタム属性を付与し、
その順番順にForeachで繰り返すことでプロパティの値を取得するソースを作成しました。

# 目次
ソースコード全文は以下の通りです。
- [ソースコード](#ソースコード)
    - [Propertyクラス](#propertyクラス)
    - [カスタム属性クラス](#カスタム属性クラス)
    - [メイン](#メイン)
    - [出力CSV](#出力csv)
 
ソースコードの解説について記載しています。
- [ソースコードの詳細](#ソースコードの詳細)
    - [Propertyクラス](#propertyクラス-1)
    - [カスタム属性クラス](#カスタム属性クラス-1)
    - [メイン](#メイン-1)

- [参考資料](#参考資料)

# ソースコード
## Propertyクラス
~~~ vb
''' <summary>
''' CSV用項目
''' </summary>
Public Class CsvProperty
    ' ヘッダー区分
    <Sort(0)>
    Property HeaderKbn As String

    ' ID
    <Sort(1)>
    Property Id As Integer

    ' 項目1
    <Sort(2)>
    Property Item1 As String

    ' 項目2
    <Sort(3)>
    Property Item2 As String

    ' 項目3
    <Sort(4)>
    Property Item3 As String
End Class
~~~

## カスタム属性クラス
~~~ vb
''' <summary>
''' カスタム属性
''' </summary>
<AttributeUsage(AttributeTargets.Property, AllowMultiple:=False, Inherited:=False)>
Public Class SortAttribute
    Inherits Attribute
    Private index As Integer
    Public Sub New(prmIndex As Integer)
        index = prmIndex
    End Sub
    Public ReadOnly Property SortIndex As Integer
        Get
            Return index
        End Get
    End Property
End Class
~~~

## メイン
~~~ vb
''' <summary>
''' メイン
''' </summary>
Public Shared Sub Main()
    ' テストデータ作成
    Dim csvList As New List(Of CsvProperty)

    Dim csvItems1 As New CsvProperty
    csvItems1.HeaderKbn = "H"
    csvItems1.Id = 1
    csvItems1.Item1 = "テスト項目1-1"
    csvItems1.Item2 = "テスト項目1-2"
    csvItems1.Item3 = "テスト項目1-3"
    csvList.Add(csvItems1)

    Dim csvItems2 As New CsvProperty
    csvItems2.HeaderKbn = "D"
    csvItems2.Id = 2
    csvItems2.Item1 = "テスト項目2-1"
    csvItems2.Item2 = "テスト項目2-2"
    csvItems2.Item3 = "テスト項目2-3"
    csvList.Add(csvItems2)

    Dim csvFilePath As String = "C:\CSV\CSVプロパティカスタム.csv"
    If IO.File.Exists(csvFilePath) Then
        ' 同名ファイルが既に存在する場合、削除
        IO.File.Delete(csvFilePath)
    End If

    ' CSVファイル書き込み
    Dim csvFile As New System.IO.StreamWriter(csvFilePath, True, Text.Encoding.GetEncoding("Shift_JIS"))

    ' CSV出力順にリスト作成
    Dim sortCsvList As List(Of String) =
        GetType(CsvProperty).GetProperties().
        OrderBy(Function(a) CType(Attribute.GetCustomAttribute(a, GetType(SortAttribute)), SortAttribute).SortIndex).
        Select(Function(a) a.Name).
        ToList()

    ' テストデータのリスト数分繰り返す
    For Each csvItems As CsvProperty In csvList
        Dim lastCnt As Integer = sortCsvList.Count
        Dim cnt As Integer = 0

        ' csv項目数分繰り返す
        For Each sortCsvItem As String In sortCsvList
            csvFile.Write("""" & CallByName(csvItems, sortCsvItem, CallType.Get) & """")
            If lastCnt - 1 > cnt Then
                csvFile.Write(","c)
            End If
            cnt += 1
        Next

        ' 改行する
        csvFile.Write(vbCrLf)
    Next

    ' CSVファイル書き込み終了
    csvFile.Close()
End Sub
~~~

## 出力CSV
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2847634/132a9b5e-e1a3-c2f1-7d53-42990383ea97.png" width="50%">

# ソースコードの詳細
## Propertyクラス
~~~ vb
    <Sort(0)>
    Property HeaderKbn As String
~~~
- \<Sort(X)>がカスタム属性
- \<SortAttribute(X)>でも記載できる

## カスタム属性クラス
~~~ vb
<AttributeUsage(AttributeTargets.Property, AllowMultiple:=False, Inherited:=False)>
~~~
- AttributeUsage 
    - カスタム属性の適用方法を制御できる属性
- AllowMultiple 
    - カスタム属性が複数回適用できる(TRUE)か、できない(FALSE)を指定できる
- Inherited 
    - カスタム属性から派生するクラスを継承する(TRUE)か、しない(FALSE)を指定できる

~~~ vb
Public Class SortAttribute
    Inherits Attribute
    Private index As Integer
    Public Sub New(prmIndex As Integer)
        index = prmIndex
    End Sub
    Public ReadOnly Property SortIndex As Integer
        Get
            Return index
        End Get
    End Property
End Class
~~~
- \<Sort(X)>のXの値がSETされ、SortIndexでGETできる

## メイン
~~~ vb
    ' CSV出力順にリスト作成
    Dim sortCsvList As List(Of String) =
        GetType(CsvProperty).GetProperties().
        OrderBy(Function(a) CType(Attribute.GetCustomAttribute(a, GetType(SortAttribute)), SortAttribute).SortIndex).
        Select(Function(a) a.Name).
        ToList()
~~~
- SortIndex順にPropertyクラスを並び替える
- プロパティ名だけのリストを作成する

~~~ vb
    csvFile.Write("""" & CallByName(csvItems, sortCsvItem, CallType.Get) & """")
~~~
- CallByNameを使用し、プロパティ名からプロパティに設定されている値を取得する
    例）以下のデータから、HeaderKbnのプロパティ名を指定し、
    　　"D"という値を取得する
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2847634/14dc01a0-56c9-2e4e-18e5-6e211047e193.png)

# 参考資料
- [カスタム属性の作成 (Visual Basic)](https://learn.microsoft.com/ja-jp/dotnet/visual-basic/programming-guide/concepts/attributes/creating-custom-attributes)
- [AttributeUsage (Visual Basic)](https://learn.microsoft.com/ja-jp/dotnet/visual-basic/programming-guide/concepts/attributes/attributeusage)
- [C#で書いたカスタム属性をVB(.net)でも書いてみる](https://running-cs.hatenablog.com/entry/2016/12/24/230900)
- [カスタム属性でメタデータを埋め込み、リフレクションにより埋め込んだメタデータを実行時に取得する (VB)](https://smdn.jp/programming/dotnet-samplecodes/reflection/3a84e390024111eb907175842ffbe222/)
- [プロフェッショナルVB.NETプログラミング　第29回 属性（後編）](https://atmarkit.itmedia.co.jp/fdotnet/vb6tonet/vb6tonet29/vb6tonet29_01.html)