ASP.NET Core で可変長なURLを作るCatch-Allパラメータの紹介

ASP.NET Core で Web アプリを作る時、

```
http://**.com/api/a/b/c/..../hoge
（※a,b,c, ... の数は可変）
```

みたいなURLを作る方法をまとめます。

# 結論

可変にしたいパラメータの頭に `*` を付ける！以上！

```c#
[HttpGet("sample/{*segments}")] 
```

参考：https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/routing?view=aspnetcore-2.2

# 使いどころ

例えば GitLab の場合、リポジトリ中のファイルを表示するページは以下のURLになります。

```
http(s)://{サーバ名}/{グループ名}/{プロジェクト名}/tree/{ブランチ名}
```

ところが、このグループは階層構造を取れるため、

- groupA
- groupA/groupB
- groupA/groupB/groupC

という形式を取れます。
これをURLにすると

- http(s)://{サーバ名}/groupA/{プロジェクト名}/tree/{ブランチ名}
- http(s)://{サーバ名}/groupA/groupB/{プロジェクト名}/tree/{ブランチ名}
- http(s)://{サーバ名}/groupA/groupB/groupC/{プロジェクト名}/tree/{ブランチ名}

となり、URLが可変長になってしまいます。

こんなURLをルーティングするWebサービスを作りたい場合には、Catch-Allパラメータが有用です。

# 実装例

```c#:ValuesController.cs
[HttpGet("sample/{*segments}")]
public ActionResult<string> GetVariableSegments([FromRoute] string segments)
{
    return segments;
}
```

```powershell
PS Z:\> Invoke-RestMethod -Method GET -Uri http://localhost:1377/api/values/sample/a/b/c
a/b/c
```

`*` をつけ忘れると、404 Not Found です。

```c#:ValuesController.cs
[HttpGet("sample/{segments}")] //*つけ忘れ
public ActionResult<string> GetVariableSegments([FromRoute] string segments)
{
    return segments;
}
```

```powershell
PS Z:\> Invoke-RestMethod -Method GET -Uri http://localhost:1377/api/values/sample/a/b/c
Invoke-RestMethod : リモート サーバーがエラーを返しました: (404) 見つかりません
発生場所 行:1 文字:1
+ Invoke-RestMethod -Method GET -Uri http://localhost:1377/api/values/g ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-RestMethod]、WebExce
    ption
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand
```


他にマッチするURLがあれば、そっちが優先されます。

```c#:ValuesController.cs
[HttpGet("sample/pre/{*segments}")]
public ActionResult<string> GetVariableSegmentsWithPre([FromRoute] string segments)
{
    return "preだよ!" + segments;
}
[HttpGet("sample/{*segments}")]
public ActionResult<string> GetVariableSegments([FromRoute] string segments)
{
    return segments;
}
[HttpGet("sample/post/{*segments}")]
public ActionResult<string> GetVariableSegmentsWithPost([FromRoute] string segments)
{
    return "postだよ!" + segments;
}
```

```powershell
PS Z:\> Invoke-RestMethod -Method GET -Uri http://localhost:1377/api/values/sample/pre/a/b/c
preだよ!a/b/c
PS Z:\> Invoke-RestMethod -Method GET -Uri http://localhost:1377/api/values/sample/post/a/b/c
postだよ!a/b/c
PS Z:\> Invoke-RestMethod -Method GET -Uri http://localhost:1377/api/values/sample/none/a/b/c
none/a/b/c
```


# 条件

Catch-Allパラメータは必ず最後のセグメントに定義されなければいけません。

`sample/{*segments}/postfix` みたいなのはダメです。以下の例外が出ます。（クエリパラメータは入ってもOK）

```
ArgumentException: A catch-all parameter can only appear as the last segment of the route template. Parameter name: routeTemplate
```

どうしてもセグメントを足したい場合、Actionメソッド内で頑張って条件分岐を書くしかなさそうです。

```c#:ValuesController.cs
[HttpGet("sample/{*segments}")]
public ActionResult<string> GetVariableSegments([FromRoute] string segments)
{
    string[] segmentsArray = segments.Split('/');
    
    //最後の一つで条件分岐
    string postfix = segmentsArray[segmentsArray.Length - 1];

    string result;
    switch (postfix)
    {
        case "hoge":
            result = "hogeだよ!";
            break;
        case "huga":
            result = "hugaだよ!";
            break;
        default:
            return new CustomJsonResult(HttpStatusCode.BadRequest, "error!");
    }
    return new CustomJsonResult(HttpStatusCode.OK, result);
}
```

以上、ASP.NET Core の Catch-All パラメータの紹介でした。