C# - プロパティに付与した属性の値を取得する処理のあるメソッドの引数に任意文字列を与えたくない

# 初めに
この記事で使用している`.NET`のバージョンは`8`です。

C#初心者が調べたことをまとめているため、不備・不足あるかもしれませんがご了承ください。

# 改善したいコード
`TargetClass`クラスの`Register`メソッドの`logicalName`には現在任意の文字列を指定できる状態で、外部からプロパティに付与した属性の値を与えている状態です。
ここでやりたいこととしては、引数に指定するのを`Person`クラスのプロパティに制限したいなと思っています。
また、その拡張として、`Person`クラスに限らないクラスが指定できるようにすることです。

```csharp
public class LogicalNameAttribute(string value) : Attribute
{
    public string Value { get; } = value;
}
```
```csharp
public class Person
{
    [LogicalName("名前")]
    public string? Name { get; set; }

    [LogicalName("年齢")]
    public int? Age { get; set; }
}
```
```csharp
// ★改善したいコード
public class TargetClass
{
    private List<string> _list = [];

    public void Register(string? logicalName, object? value)
    {
        ArgumentNullException.ThrowIfNull(logicalName);
        ArgumentNullException.ThrowIfNull(value);
        _list.Add($"{logicalName} ： {value}");
    }

    public string Output() => string.Join(", ", _list);
}
```
```csharp
using System.Reflection;

public class Program
{
    public static void Main(string[] args)
    {
        var nameP = typeof(Person).GetProperty(nameof(Person.Name));
        var nameLN = nameP?.GetCustomAttribute<LogicalNameAttribute>()?.Value;
        var ageP = typeof(Person).GetProperty(nameof(Person.Age));
        var ageLN = ageP?.GetCustomAttribute<LogicalNameAttribute>()?.Value;

        var target = new TargetClass();
        target.Register(nameLN, "john");
        target.Register(ageLN, 21);
        Console.WriteLine(target.Output());
    }
}
```
↓出力結果
```
名前 ： john, 年齢 ： 21
```

# 改善コード案１
`TargerClass`クラスにジェネリクスを導入してみました。使用例(`Program`クラス)ではPersonクラスを指定しています。
`Register`メソッドにプロパティ名を指定することで、Personクラスにその名をもつプロパティがあれば、属性から値を取得してくれる処理になってます。
```csharp
public class Program
{
    public static void Main(string[] args)
    {
        var target = new TargetClass<Person>();
        target.Register(nameof(Person.Name), "john");
        target.Register(nameof(Person.Age), 21);
        Console.WriteLine(target.Output());
    }
}
```
```csharp
public class TargetClass<T>
{
    private List<string> _list = [];

    public void Register(string? propertyName, object? value)
    {
        ArgumentNullException.ThrowIfNull(propertyName);
        ArgumentNullException.ThrowIfNull(value);

        var property = typeof(T).GetProperty(propertyName);
        var logicalName = (property?.GetCustomAttribute<LogicalNameAttribute>()?.Value) ?? throw new Exception();
        _list.Add($"{logicalName} ： {value}");
    }

    public string Output() => string.Join(", ", _list);
}
```

プロパティ名として、「nameof(Person.Name)」のように指定しているものの、引数の型としては`string`なので、好きに文字列を与えることができてしまいます。
どうにか、`Register`メソッドに与える時点で、Personクラスに制限することはできないでしょうか？

# 改善コード案２
`Person.Name`のように指定する方法はわからなかったものの、「プロパティを呼ぶラムダ式」を指定することで疑似的にプロパティを指定する改善案です。
```csharp
public class Program
{
    public static void Main(string[] args)
    {
        var target = new TargetClass<Person>();
        target.Register(x => x.Name, "john");
        target.Register(x => x.Age, 21);
        Console.WriteLine(target.Output());
    }
}
```
```csharp
public class TargetClass<T>
{
    private List<string> _list = [];

    public void Register<U>(Expression<Func<T, U>> expression, U value)
    {
        ArgumentNullException.ThrowIfNull(expression);
        ArgumentNullException.ThrowIfNull(value);

        var memberExpression = expression?.Body as MemberExpression;
        var property = memberExpression?.Member as PropertyInfo;
        var logicalName = property?.GetCustomAttribute<LogicalNameAttribute>()?.Value ?? throw new Exception();
        _list.Add($"{logicalName} ： {value}");
    }

    public string Output() => string.Join(", ", _list);
}
```
ラムダ式の引数の型が`TargerClass`へ指定した総称型になってるので、それに関係のないプロパティ・名を指定するミスはなくなるかと思います。

# 最後に
これ以外の改善案は現状わからないですね。
例えば速度面とかは度外視な改善案なので、どう実装するかはその時のスキルや環境に応じて決めていく感じでしょうか。

# 参考
- [C# - way to pass property of the generic type like an enum - Stack Overflow ](https://stackoverflow.com/questions/73068065/c-sharp-way-to-pass-property-of-the-generic-type-like-an-enum)

# 関連
- [C# - プロパティに付与した属性の値を取得したいときにプロパティ名を文字列で指定したくない](https://qiita.com/Takowasa_Naan/items/8ec9a312315fb798acae?utm_campaign=email&utm_content=link&utm_medium=email&utm_source=public_comment#comment-efdfa8d5b5c6a09c9397)