Add test suite to analyze ASP.NET Core routing scenarios (#4950)

This commit is contained in:
Alan West 2023-11-09 09:57:55 -08:00 committed by GitHub
parent f25ff3a4f4
commit 0a989a942f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 3342 additions and 0 deletions

View File

@ -98,13 +98,16 @@
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="6.0.21" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.21" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.21" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="7.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="7.0.9" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="8.0.0-rc.2.23480.2" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0-rc.2.23480.2" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.0-rc.2.23480.2" />
</ItemGroup>
</Project>

View File

@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
@ -36,4 +37,11 @@
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Shared\InMemoryEventListener.cs" Link="Includes\InMemoryEventListener.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Tests\Shared\TestEventListener.cs" Link="Includes\TestEventListener.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="RouteTests\RoutingTestCases.json">
<LogicalName>RoutingTestCases.json</LogicalName>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@ -0,0 +1,204 @@
# ASP.NET Core `http.route` tests
This folder contains a test suite that validates the instrumentation produces
the expected `http.route` attribute on both the activity and metric it emits.
When available, the `http.route` is also a required component of the
`Activity.DisplayName`.
The test suite covers a variety of different routing scenarios available for
ASP.NET Core:
* [Conventional routing](https://learn.microsoft.com/aspnet/core/mvc/controllers/routing#conventional-routing)
* [Conventional routing using areas](https://learn.microsoft.com/aspnet/core/mvc/controllers/routing#areas)
* [Attribute routing](https://learn.microsoft.com/aspnet/core/mvc/controllers/routing#attribute-routing-for-rest-apis)
* [Razor pages](https://learn.microsoft.com/aspnet/core/razor-pages/razor-pages-conventions)
* [Minimal APIs](https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis/route-handlers)
The individual test cases are defined in RoutingTestCases.json.
The test suite is unique in that, when run, it generates README files for each
target framework which aids in documenting how the instrumentation behaves for
each test case. These files are source-controlled, so if the behavior of the
instrumentation changes, the README files will be updated to reflect the change.
* [.NET 6](./README.net6.0.md)
* [.NET 7](./README.net7.0.md)
* [.NET 8](./README.net8.0.md)
For each test case a request is made to an ASP.NET Core application with a
particular routing configuration. ASP.NET Core offers a
[variety of APIs](#aspnet-core-apis-for-retrieving-route-information) for
retrieving the route information of a given request. The README files include
detailed information documenting the route information available using the
various APIs in each test case. For example, here is the detailed result
generated for a test case:
```json
{
"IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}",
"ActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter/2",
"ActivityHttpRoute": "",
"MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/ConventionalRoute/ActionWithStringParameter/2?num=3",
"RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"controller": "ConventionalRoute",
"action": "ActionWithStringParameter",
"id": "2"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": null,
"Parameters": [
"id",
"num"
],
"ControllerActionDescriptor": {
"ControllerName": "ConventionalRoute",
"ActionName": "ActionWithStringParameter"
},
"PageActionDescriptor": null
}
}
}
```
> [!NOTE]
> The test result currently includes an `IdealHttpRoute` property. This is
> temporary, and is meant to drive a conversation to determine the best way
> for generating the `http.route` attribute under different routing scenarios.
> In the example above, the path invoked is
> `/ConventionalRoute/ActionWithStringParameter/2?num=3`. Currently, we see
> that the `http.route` attribute on the metric emitted is
> `{controller=ConventionalRoute}/{action=Default}/{id?}` which was derived
> using `RoutePattern.RawText`. This is not ideal
> because the route template does not include the actual action that was
> invoked `ActionWithStringParameter`. The invoked action could be derived
> using either the `ControllerActionDescriptor`
> or `HttpContext.GetRouteData()`.
## ASP.NET Core APIs for retrieving route information
Included below are short snippets illustrating the use of the various
APIs available for retrieving route information.
### Retrieving the route template
The route template can be obtained from `HttpContext` by retrieving the
`RouteEndpoint` using the following two APIs.
For attribute routing and minimal API scenarios, using the route template alone
is sufficient for deriving `http.route` in all test cases.
The route template does not well describe the `http.route` in conventional
routing and some Razor page scenarios.
#### [RoutePattern.RawText](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.routing.patterns.routepattern.rawtext)
```csharp
(httpContext.GetEndpoint() as RouteEndpoint)?.RoutePattern.RawText;
```
#### [IRouteDiagnosticsMetadata.Route](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.metadata.iroutediagnosticsmetadata.route)
This API was introduced in .NET 8.
```csharp
httpContext.GetEndpoint()?.Metadata.GetMetadata<IRouteDiagnosticsMetadata>()?.Route;
```
### RouteData
`RouteData` can be retrieved from `HttpContext` using the `GetRouteData()`
extension method. The values obtained from `RouteData` identify the controller/
action or Razor page invoked by the request.
#### [HttpContext.GetRouteData()](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.routing.routinghttpcontextextensions.getroutedata)
```csharp
foreach (var value in httpContext.GetRouteData().Values)
{
Console.WriteLine($"{value.Key} = {value.Value?.ToString()}");
}
```
For example, the above code produces something like:
```text
controller = ConventionalRoute
action = ActionWithStringParameter
id = 2
```
### Information from the ActionDescriptor
For requests that invoke an action or Razor page, the `ActionDescriptor` can
be used to access route information.
#### [AttributeRouteInfo.Template](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.mvc.routing.attributerouteinfo.template)
The `AttributeRouteInfo.Template` is equivalent to using
[other APIs for retrieving the route template](#retrieving-the-route-template)
when using attribute routing. For conventional routing and Razor pages it will
be `null`.
```csharp
actionDescriptor.AttributeRouteInfo?.Template;
```
#### [ControllerActionDescriptor](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.mvc.controllers.controlleractiondescriptor)
For requests that invoke an action on a controller, the `ActionDescriptor`
will be of type `ControllerActionDescriptor` which includes the controller and
action name.
```csharp
(actionDescriptor as ControllerActionDescriptor)?.ControllerName;
(actionDescriptor as ControllerActionDescriptor)?.ActionName;
```
#### [PageActionDescriptor](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pageactiondescriptor)
For requests that invoke a Razor page, the `ActionDescriptor`
will be of type `PageActionDescriptor` which includes the path to the invoked
page.
```csharp
(actionDescriptor as PageActionDescriptor)?.RelativePath;
(actionDescriptor as PageActionDescriptor)?.ViewEnginePath;
```
#### [Parameters](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.mvc.abstractions.actiondescriptor.parameters#microsoft-aspnetcore-mvc-abstractions-actiondescriptor-parameters)
The `ActionDescriptor.Parameters` property is interesting because it describes
the actual parameters (type and name) of an invoked action method. Some APM
products use `ActionDescriptor.Parameters` to more precisely describe the
method an endpoint invokes since not all parameters may be present in the
route template.
Consider the following action method:
```csharp
public IActionResult SomeActionMethod(string id, int num) { ... }
```
Using conventional routing assuming a default route template
`{controller=ConventionalRoute}/{action=Default}/{id?}`, the `SomeActionMethod`
may match this route template. The route template describes the `id` parameter
but not the `num` parameter.
```csharp
foreach (var parameter in actionDescriptor.Parameters)
{
Console.WriteLine($"{parameter.Name}");
}
```
The above code produces:
```text
id
num
```

View File

@ -0,0 +1,592 @@
# Test results for ASP.NET Core 6
| Span http.route | Metric http.route | App | Test Name |
| - | - | - | - |
| :broken_heart: | :broken_heart: | ConventionalRouting | [Root path](#conventionalrouting-root-path) |
| :broken_heart: | :broken_heart: | ConventionalRouting | [Non-default action with route parameter and query string](#conventionalrouting-non-default-action-with-route-parameter-and-query-string) |
| :broken_heart: | :broken_heart: | ConventionalRouting | [Non-default action with query string](#conventionalrouting-non-default-action-with-query-string) |
| :broken_heart: | :broken_heart: | ConventionalRouting | [Not Found (404)](#conventionalrouting-not-found-404) |
| :broken_heart: | :green_heart: | ConventionalRouting | [Route template with parameter constraint](#conventionalrouting-route-template-with-parameter-constraint) |
| :broken_heart: | :green_heart: | ConventionalRouting | [Path that does not match parameter constraint](#conventionalrouting-path-that-does-not-match-parameter-constraint) |
| :broken_heart: | :broken_heart: | ConventionalRouting | [Area using area:exists, default controller/action](#conventionalrouting-area-using-areaexists-default-controlleraction) |
| :broken_heart: | :broken_heart: | ConventionalRouting | [Area using area:exists, non-default action](#conventionalrouting-area-using-areaexists-non-default-action) |
| :broken_heart: | :broken_heart: | ConventionalRouting | [Area w/o area:exists, default controller/action](#conventionalrouting-area-wo-areaexists-default-controlleraction) |
| :green_heart: | :green_heart: | AttributeRouting | [Default action](#attributerouting-default-action) |
| :green_heart: | :green_heart: | AttributeRouting | [Action without parameter](#attributerouting-action-without-parameter) |
| :green_heart: | :green_heart: | AttributeRouting | [Action with parameter](#attributerouting-action-with-parameter) |
| :green_heart: | :green_heart: | AttributeRouting | [Action with parameter before action name in template](#attributerouting-action-with-parameter-before-action-name-in-template) |
| :green_heart: | :green_heart: | AttributeRouting | [Action invoked resulting in 400 Bad Request](#attributerouting-action-invoked-resulting-in-400-bad-request) |
| :broken_heart: | :broken_heart: | RazorPages | [Root path](#razorpages-root-path) |
| :broken_heart: | :broken_heart: | RazorPages | [Index page](#razorpages-index-page) |
| :broken_heart: | :broken_heart: | RazorPages | [Throws exception](#razorpages-throws-exception) |
| :green_heart: | :green_heart: | RazorPages | [Static content](#razorpages-static-content) |
| :broken_heart: | :green_heart: | MinimalApi | [Action without parameter](#minimalapi-action-without-parameter) |
| :broken_heart: | :green_heart: | MinimalApi | [Action with parameter](#minimalapi-action-with-parameter) |
## ConventionalRouting: Root path
```json
{
"IdealHttpRoute": "ConventionalRoute/Default/{id?}",
"ActivityDisplayName": "/",
"ActivityHttpRoute": "",
"MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/",
"RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"controller": "ConventionalRoute",
"action": "Default"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": null,
"Parameters": [],
"ControllerActionDescriptor": {
"ControllerName": "ConventionalRoute",
"ActionName": "Default"
},
"PageActionDescriptor": null
}
}
}
```
## ConventionalRouting: Non-default action with route parameter and query string
```json
{
"IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}",
"ActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter/2",
"ActivityHttpRoute": "",
"MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/ConventionalRoute/ActionWithStringParameter/2?num=3",
"RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"controller": "ConventionalRoute",
"action": "ActionWithStringParameter",
"id": "2"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": null,
"Parameters": [
"id",
"num"
],
"ControllerActionDescriptor": {
"ControllerName": "ConventionalRoute",
"ActionName": "ActionWithStringParameter"
},
"PageActionDescriptor": null
}
}
}
```
## ConventionalRouting: Non-default action with query string
```json
{
"IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}",
"ActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter",
"ActivityHttpRoute": "",
"MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/ConventionalRoute/ActionWithStringParameter?num=3",
"RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"controller": "ConventionalRoute",
"action": "ActionWithStringParameter"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": null,
"Parameters": [
"id",
"num"
],
"ControllerActionDescriptor": {
"ControllerName": "ConventionalRoute",
"ActionName": "ActionWithStringParameter"
},
"PageActionDescriptor": null
}
}
}
```
## ConventionalRouting: Not Found (404)
```json
{
"IdealHttpRoute": "",
"ActivityDisplayName": "/ConventionalRoute/NotFound",
"ActivityHttpRoute": "",
"MetricHttpRoute": "",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/ConventionalRoute/NotFound",
"RoutePattern.RawText": null,
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {},
"ActionDescriptor": null
}
}
```
## ConventionalRouting: Route template with parameter constraint
```json
{
"IdealHttpRoute": "SomePath/{id}/{num:int}",
"ActivityDisplayName": "/SomePath/SomeString/2",
"ActivityHttpRoute": "",
"MetricHttpRoute": "SomePath/{id}/{num:int}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/SomePath/SomeString/2",
"RoutePattern.RawText": "SomePath/{id}/{num:int}",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"controller": "ConventionalRoute",
"action": "ActionWithStringParameter",
"id": "SomeString",
"num": "2"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": null,
"Parameters": [
"id",
"num"
],
"ControllerActionDescriptor": {
"ControllerName": "ConventionalRoute",
"ActionName": "ActionWithStringParameter"
},
"PageActionDescriptor": null
}
}
}
```
## ConventionalRouting: Path that does not match parameter constraint
```json
{
"IdealHttpRoute": "",
"ActivityDisplayName": "/SomePath/SomeString/NotAnInt",
"ActivityHttpRoute": "",
"MetricHttpRoute": "",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/SomePath/SomeString/NotAnInt",
"RoutePattern.RawText": null,
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {},
"ActionDescriptor": null
}
}
```
## ConventionalRouting: Area using area:exists, default controller/action
```json
{
"IdealHttpRoute": "{area:exists}/ControllerForMyArea/Default/{id?}",
"ActivityDisplayName": "/MyArea",
"ActivityHttpRoute": "",
"MetricHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/MyArea",
"RoutePattern.RawText": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"controller": "ControllerForMyArea",
"action": "Default",
"area": "MyArea"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": null,
"Parameters": [],
"ControllerActionDescriptor": {
"ControllerName": "ControllerForMyArea",
"ActionName": "Default"
},
"PageActionDescriptor": null
}
}
}
```
## ConventionalRouting: Area using area:exists, non-default action
```json
{
"IdealHttpRoute": "{area:exists}/ControllerForMyArea/NonDefault/{id?}",
"ActivityDisplayName": "/MyArea/ControllerForMyArea/NonDefault",
"ActivityHttpRoute": "",
"MetricHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/MyArea/ControllerForMyArea/NonDefault",
"RoutePattern.RawText": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"controller": "ControllerForMyArea",
"area": "MyArea",
"action": "NonDefault"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": null,
"Parameters": [],
"ControllerActionDescriptor": {
"ControllerName": "ControllerForMyArea",
"ActionName": "NonDefault"
},
"PageActionDescriptor": null
}
}
}
```
## ConventionalRouting: Area w/o area:exists, default controller/action
```json
{
"IdealHttpRoute": "SomePrefix/AnotherArea/Index/{id?}",
"ActivityDisplayName": "/SomePrefix",
"ActivityHttpRoute": "",
"MetricHttpRoute": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/SomePrefix",
"RoutePattern.RawText": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"area": "AnotherArea",
"controller": "AnotherArea",
"action": "Index"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": null,
"Parameters": [],
"ControllerActionDescriptor": {
"ControllerName": "AnotherArea",
"ActionName": "Index"
},
"PageActionDescriptor": null
}
}
}
```
## AttributeRouting: Default action
```json
{
"IdealHttpRoute": "AttributeRoute",
"ActivityDisplayName": "AttributeRoute",
"ActivityHttpRoute": "AttributeRoute",
"MetricHttpRoute": "AttributeRoute",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/AttributeRoute",
"RoutePattern.RawText": "AttributeRoute",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"action": "Get",
"controller": "AttributeRoute"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": "AttributeRoute",
"Parameters": [],
"ControllerActionDescriptor": {
"ControllerName": "AttributeRoute",
"ActionName": "Get"
},
"PageActionDescriptor": null
}
}
}
```
## AttributeRouting: Action without parameter
```json
{
"IdealHttpRoute": "AttributeRoute/Get",
"ActivityDisplayName": "AttributeRoute/Get",
"ActivityHttpRoute": "AttributeRoute/Get",
"MetricHttpRoute": "AttributeRoute/Get",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/AttributeRoute/Get",
"RoutePattern.RawText": "AttributeRoute/Get",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"action": "Get",
"controller": "AttributeRoute"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": "AttributeRoute/Get",
"Parameters": [],
"ControllerActionDescriptor": {
"ControllerName": "AttributeRoute",
"ActionName": "Get"
},
"PageActionDescriptor": null
}
}
}
```
## AttributeRouting: Action with parameter
```json
{
"IdealHttpRoute": "AttributeRoute/Get/{id}",
"ActivityDisplayName": "AttributeRoute/Get/{id}",
"ActivityHttpRoute": "AttributeRoute/Get/{id}",
"MetricHttpRoute": "AttributeRoute/Get/{id}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/AttributeRoute/Get/12",
"RoutePattern.RawText": "AttributeRoute/Get/{id}",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"action": "Get",
"controller": "AttributeRoute",
"id": "12"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": "AttributeRoute/Get/{id}",
"Parameters": [
"id"
],
"ControllerActionDescriptor": {
"ControllerName": "AttributeRoute",
"ActionName": "Get"
},
"PageActionDescriptor": null
}
}
}
```
## AttributeRouting: Action with parameter before action name in template
```json
{
"IdealHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"ActivityDisplayName": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"ActivityHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"MetricHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/AttributeRoute/12/GetWithActionNameInDifferentSpotInTemplate",
"RoutePattern.RawText": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"action": "GetWithActionNameInDifferentSpotInTemplate",
"controller": "AttributeRoute",
"id": "12"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"Parameters": [
"id"
],
"ControllerActionDescriptor": {
"ControllerName": "AttributeRoute",
"ActionName": "GetWithActionNameInDifferentSpotInTemplate"
},
"PageActionDescriptor": null
}
}
}
```
## AttributeRouting: Action invoked resulting in 400 Bad Request
```json
{
"IdealHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"ActivityDisplayName": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"ActivityHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"MetricHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/AttributeRoute/NotAnInt/GetWithActionNameInDifferentSpotInTemplate",
"RoutePattern.RawText": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"action": "GetWithActionNameInDifferentSpotInTemplate",
"controller": "AttributeRoute",
"id": "NotAnInt"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"Parameters": [
"id"
],
"ControllerActionDescriptor": {
"ControllerName": "AttributeRoute",
"ActionName": "GetWithActionNameInDifferentSpotInTemplate"
},
"PageActionDescriptor": null
}
}
}
```
## RazorPages: Root path
```json
{
"IdealHttpRoute": "/Index",
"ActivityDisplayName": "/",
"ActivityHttpRoute": "",
"MetricHttpRoute": "",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/",
"RoutePattern.RawText": "",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"page": "/Index"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": "",
"Parameters": [],
"ControllerActionDescriptor": null,
"PageActionDescriptor": {
"RelativePath": "/Pages/Index.cshtml",
"ViewEnginePath": "/Index"
}
}
}
}
```
## RazorPages: Index page
```json
{
"IdealHttpRoute": "/Index",
"ActivityDisplayName": "Index",
"ActivityHttpRoute": "Index",
"MetricHttpRoute": "Index",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/Index",
"RoutePattern.RawText": "Index",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"page": "/Index"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": "Index",
"Parameters": [],
"ControllerActionDescriptor": null,
"PageActionDescriptor": {
"RelativePath": "/Pages/Index.cshtml",
"ViewEnginePath": "/Index"
}
}
}
}
```
## RazorPages: Throws exception
```json
{
"IdealHttpRoute": "/PageThatThrowsException",
"ActivityDisplayName": "PageThatThrowsException",
"ActivityHttpRoute": "PageThatThrowsException",
"MetricHttpRoute": "PageThatThrowsException",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/PageThatThrowsException",
"RoutePattern.RawText": "PageThatThrowsException",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"page": "/PageThatThrowsException"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": "PageThatThrowsException",
"Parameters": [],
"ControllerActionDescriptor": null,
"PageActionDescriptor": {
"RelativePath": "/Pages/PageThatThrowsException.cshtml",
"ViewEnginePath": "/PageThatThrowsException"
}
}
}
}
```
## RazorPages: Static content
```json
{
"IdealHttpRoute": "",
"ActivityDisplayName": "/js/site.js",
"ActivityHttpRoute": "",
"MetricHttpRoute": "",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/js/site.js",
"RoutePattern.RawText": null,
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {},
"ActionDescriptor": null
}
}
```
## MinimalApi: Action without parameter
```json
{
"IdealHttpRoute": "/MinimalApi",
"ActivityDisplayName": "/MinimalApi",
"ActivityHttpRoute": "",
"MetricHttpRoute": "/MinimalApi",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/MinimalApi",
"RoutePattern.RawText": "/MinimalApi",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {},
"ActionDescriptor": null
}
}
```
## MinimalApi: Action with parameter
```json
{
"IdealHttpRoute": "/MinimalApi/{id}",
"ActivityDisplayName": "/MinimalApi/123",
"ActivityHttpRoute": "",
"MetricHttpRoute": "/MinimalApi/{id}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/MinimalApi/123",
"RoutePattern.RawText": "/MinimalApi/{id}",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"id": "123"
},
"ActionDescriptor": null
}
}
```

View File

@ -0,0 +1,634 @@
# Test results for ASP.NET Core 7
| Span http.route | Metric http.route | App | Test Name |
| - | - | - | - |
| :broken_heart: | :broken_heart: | ConventionalRouting | [Root path](#conventionalrouting-root-path) |
| :broken_heart: | :broken_heart: | ConventionalRouting | [Non-default action with route parameter and query string](#conventionalrouting-non-default-action-with-route-parameter-and-query-string) |
| :broken_heart: | :broken_heart: | ConventionalRouting | [Non-default action with query string](#conventionalrouting-non-default-action-with-query-string) |
| :broken_heart: | :broken_heart: | ConventionalRouting | [Not Found (404)](#conventionalrouting-not-found-404) |
| :broken_heart: | :green_heart: | ConventionalRouting | [Route template with parameter constraint](#conventionalrouting-route-template-with-parameter-constraint) |
| :broken_heart: | :green_heart: | ConventionalRouting | [Path that does not match parameter constraint](#conventionalrouting-path-that-does-not-match-parameter-constraint) |
| :broken_heart: | :broken_heart: | ConventionalRouting | [Area using area:exists, default controller/action](#conventionalrouting-area-using-areaexists-default-controlleraction) |
| :broken_heart: | :broken_heart: | ConventionalRouting | [Area using area:exists, non-default action](#conventionalrouting-area-using-areaexists-non-default-action) |
| :broken_heart: | :broken_heart: | ConventionalRouting | [Area w/o area:exists, default controller/action](#conventionalrouting-area-wo-areaexists-default-controlleraction) |
| :green_heart: | :green_heart: | AttributeRouting | [Default action](#attributerouting-default-action) |
| :green_heart: | :green_heart: | AttributeRouting | [Action without parameter](#attributerouting-action-without-parameter) |
| :green_heart: | :green_heart: | AttributeRouting | [Action with parameter](#attributerouting-action-with-parameter) |
| :green_heart: | :green_heart: | AttributeRouting | [Action with parameter before action name in template](#attributerouting-action-with-parameter-before-action-name-in-template) |
| :green_heart: | :green_heart: | AttributeRouting | [Action invoked resulting in 400 Bad Request](#attributerouting-action-invoked-resulting-in-400-bad-request) |
| :broken_heart: | :broken_heart: | RazorPages | [Root path](#razorpages-root-path) |
| :broken_heart: | :broken_heart: | RazorPages | [Index page](#razorpages-index-page) |
| :broken_heart: | :broken_heart: | RazorPages | [Throws exception](#razorpages-throws-exception) |
| :green_heart: | :green_heart: | RazorPages | [Static content](#razorpages-static-content) |
| :broken_heart: | :green_heart: | MinimalApi | [Action without parameter](#minimalapi-action-without-parameter) |
| :broken_heart: | :green_heart: | MinimalApi | [Action with parameter](#minimalapi-action-with-parameter) |
| :broken_heart: | :green_heart: | MinimalApi | [Action without parameter (MapGroup)](#minimalapi-action-without-parameter-mapgroup) |
| :broken_heart: | :green_heart: | MinimalApi | [Action with parameter (MapGroup)](#minimalapi-action-with-parameter-mapgroup) |
## ConventionalRouting: Root path
```json
{
"IdealHttpRoute": "ConventionalRoute/Default/{id?}",
"ActivityDisplayName": "/",
"ActivityHttpRoute": "",
"MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/",
"RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"controller": "ConventionalRoute",
"action": "Default"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": null,
"Parameters": [],
"ControllerActionDescriptor": {
"ControllerName": "ConventionalRoute",
"ActionName": "Default"
},
"PageActionDescriptor": null
}
}
}
```
## ConventionalRouting: Non-default action with route parameter and query string
```json
{
"IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}",
"ActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter/2",
"ActivityHttpRoute": "",
"MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/ConventionalRoute/ActionWithStringParameter/2?num=3",
"RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"controller": "ConventionalRoute",
"action": "ActionWithStringParameter",
"id": "2"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": null,
"Parameters": [
"id",
"num"
],
"ControllerActionDescriptor": {
"ControllerName": "ConventionalRoute",
"ActionName": "ActionWithStringParameter"
},
"PageActionDescriptor": null
}
}
}
```
## ConventionalRouting: Non-default action with query string
```json
{
"IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}",
"ActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter",
"ActivityHttpRoute": "",
"MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/ConventionalRoute/ActionWithStringParameter?num=3",
"RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"controller": "ConventionalRoute",
"action": "ActionWithStringParameter"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": null,
"Parameters": [
"id",
"num"
],
"ControllerActionDescriptor": {
"ControllerName": "ConventionalRoute",
"ActionName": "ActionWithStringParameter"
},
"PageActionDescriptor": null
}
}
}
```
## ConventionalRouting: Not Found (404)
```json
{
"IdealHttpRoute": "",
"ActivityDisplayName": "/ConventionalRoute/NotFound",
"ActivityHttpRoute": "",
"MetricHttpRoute": "",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/ConventionalRoute/NotFound",
"RoutePattern.RawText": null,
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {},
"ActionDescriptor": null
}
}
```
## ConventionalRouting: Route template with parameter constraint
```json
{
"IdealHttpRoute": "SomePath/{id}/{num:int}",
"ActivityDisplayName": "/SomePath/SomeString/2",
"ActivityHttpRoute": "",
"MetricHttpRoute": "SomePath/{id}/{num:int}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/SomePath/SomeString/2",
"RoutePattern.RawText": "SomePath/{id}/{num:int}",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"controller": "ConventionalRoute",
"action": "ActionWithStringParameter",
"id": "SomeString",
"num": "2"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": null,
"Parameters": [
"id",
"num"
],
"ControllerActionDescriptor": {
"ControllerName": "ConventionalRoute",
"ActionName": "ActionWithStringParameter"
},
"PageActionDescriptor": null
}
}
}
```
## ConventionalRouting: Path that does not match parameter constraint
```json
{
"IdealHttpRoute": "",
"ActivityDisplayName": "/SomePath/SomeString/NotAnInt",
"ActivityHttpRoute": "",
"MetricHttpRoute": "",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/SomePath/SomeString/NotAnInt",
"RoutePattern.RawText": null,
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {},
"ActionDescriptor": null
}
}
```
## ConventionalRouting: Area using area:exists, default controller/action
```json
{
"IdealHttpRoute": "{area:exists}/ControllerForMyArea/Default/{id?}",
"ActivityDisplayName": "/MyArea",
"ActivityHttpRoute": "",
"MetricHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/MyArea",
"RoutePattern.RawText": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"controller": "ControllerForMyArea",
"action": "Default",
"area": "MyArea"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": null,
"Parameters": [],
"ControllerActionDescriptor": {
"ControllerName": "ControllerForMyArea",
"ActionName": "Default"
},
"PageActionDescriptor": null
}
}
}
```
## ConventionalRouting: Area using area:exists, non-default action
```json
{
"IdealHttpRoute": "{area:exists}/ControllerForMyArea/NonDefault/{id?}",
"ActivityDisplayName": "/MyArea/ControllerForMyArea/NonDefault",
"ActivityHttpRoute": "",
"MetricHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/MyArea/ControllerForMyArea/NonDefault",
"RoutePattern.RawText": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"controller": "ControllerForMyArea",
"area": "MyArea",
"action": "NonDefault"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": null,
"Parameters": [],
"ControllerActionDescriptor": {
"ControllerName": "ControllerForMyArea",
"ActionName": "NonDefault"
},
"PageActionDescriptor": null
}
}
}
```
## ConventionalRouting: Area w/o area:exists, default controller/action
```json
{
"IdealHttpRoute": "SomePrefix/AnotherArea/Index/{id?}",
"ActivityDisplayName": "/SomePrefix",
"ActivityHttpRoute": "",
"MetricHttpRoute": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/SomePrefix",
"RoutePattern.RawText": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"area": "AnotherArea",
"controller": "AnotherArea",
"action": "Index"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": null,
"Parameters": [],
"ControllerActionDescriptor": {
"ControllerName": "AnotherArea",
"ActionName": "Index"
},
"PageActionDescriptor": null
}
}
}
```
## AttributeRouting: Default action
```json
{
"IdealHttpRoute": "AttributeRoute",
"ActivityDisplayName": "AttributeRoute",
"ActivityHttpRoute": "AttributeRoute",
"MetricHttpRoute": "AttributeRoute",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/AttributeRoute",
"RoutePattern.RawText": "AttributeRoute",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"action": "Get",
"controller": "AttributeRoute"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": "AttributeRoute",
"Parameters": [],
"ControllerActionDescriptor": {
"ControllerName": "AttributeRoute",
"ActionName": "Get"
},
"PageActionDescriptor": null
}
}
}
```
## AttributeRouting: Action without parameter
```json
{
"IdealHttpRoute": "AttributeRoute/Get",
"ActivityDisplayName": "AttributeRoute/Get",
"ActivityHttpRoute": "AttributeRoute/Get",
"MetricHttpRoute": "AttributeRoute/Get",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/AttributeRoute/Get",
"RoutePattern.RawText": "AttributeRoute/Get",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"action": "Get",
"controller": "AttributeRoute"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": "AttributeRoute/Get",
"Parameters": [],
"ControllerActionDescriptor": {
"ControllerName": "AttributeRoute",
"ActionName": "Get"
},
"PageActionDescriptor": null
}
}
}
```
## AttributeRouting: Action with parameter
```json
{
"IdealHttpRoute": "AttributeRoute/Get/{id}",
"ActivityDisplayName": "AttributeRoute/Get/{id}",
"ActivityHttpRoute": "AttributeRoute/Get/{id}",
"MetricHttpRoute": "AttributeRoute/Get/{id}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/AttributeRoute/Get/12",
"RoutePattern.RawText": "AttributeRoute/Get/{id}",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"action": "Get",
"controller": "AttributeRoute",
"id": "12"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": "AttributeRoute/Get/{id}",
"Parameters": [
"id"
],
"ControllerActionDescriptor": {
"ControllerName": "AttributeRoute",
"ActionName": "Get"
},
"PageActionDescriptor": null
}
}
}
```
## AttributeRouting: Action with parameter before action name in template
```json
{
"IdealHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"ActivityDisplayName": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"ActivityHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"MetricHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/AttributeRoute/12/GetWithActionNameInDifferentSpotInTemplate",
"RoutePattern.RawText": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"action": "GetWithActionNameInDifferentSpotInTemplate",
"controller": "AttributeRoute",
"id": "12"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"Parameters": [
"id"
],
"ControllerActionDescriptor": {
"ControllerName": "AttributeRoute",
"ActionName": "GetWithActionNameInDifferentSpotInTemplate"
},
"PageActionDescriptor": null
}
}
}
```
## AttributeRouting: Action invoked resulting in 400 Bad Request
```json
{
"IdealHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"ActivityDisplayName": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"ActivityHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"MetricHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/AttributeRoute/NotAnInt/GetWithActionNameInDifferentSpotInTemplate",
"RoutePattern.RawText": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"action": "GetWithActionNameInDifferentSpotInTemplate",
"controller": "AttributeRoute",
"id": "NotAnInt"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"Parameters": [
"id"
],
"ControllerActionDescriptor": {
"ControllerName": "AttributeRoute",
"ActionName": "GetWithActionNameInDifferentSpotInTemplate"
},
"PageActionDescriptor": null
}
}
}
```
## RazorPages: Root path
```json
{
"IdealHttpRoute": "/Index",
"ActivityDisplayName": "/",
"ActivityHttpRoute": "",
"MetricHttpRoute": "",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/",
"RoutePattern.RawText": "",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"page": "/Index"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": "",
"Parameters": [],
"ControllerActionDescriptor": null,
"PageActionDescriptor": {
"RelativePath": "/Pages/Index.cshtml",
"ViewEnginePath": "/Index"
}
}
}
}
```
## RazorPages: Index page
```json
{
"IdealHttpRoute": "/Index",
"ActivityDisplayName": "Index",
"ActivityHttpRoute": "Index",
"MetricHttpRoute": "Index",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/Index",
"RoutePattern.RawText": "Index",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"page": "/Index"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": "Index",
"Parameters": [],
"ControllerActionDescriptor": null,
"PageActionDescriptor": {
"RelativePath": "/Pages/Index.cshtml",
"ViewEnginePath": "/Index"
}
}
}
}
```
## RazorPages: Throws exception
```json
{
"IdealHttpRoute": "/PageThatThrowsException",
"ActivityDisplayName": "PageThatThrowsException",
"ActivityHttpRoute": "PageThatThrowsException",
"MetricHttpRoute": "PageThatThrowsException",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/PageThatThrowsException",
"RoutePattern.RawText": "PageThatThrowsException",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"page": "/PageThatThrowsException"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": "PageThatThrowsException",
"Parameters": [],
"ControllerActionDescriptor": null,
"PageActionDescriptor": {
"RelativePath": "/Pages/PageThatThrowsException.cshtml",
"ViewEnginePath": "/PageThatThrowsException"
}
}
}
}
```
## RazorPages: Static content
```json
{
"IdealHttpRoute": "",
"ActivityDisplayName": "/js/site.js",
"ActivityHttpRoute": "",
"MetricHttpRoute": "",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/js/site.js",
"RoutePattern.RawText": null,
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {},
"ActionDescriptor": null
}
}
```
## MinimalApi: Action without parameter
```json
{
"IdealHttpRoute": "/MinimalApi",
"ActivityDisplayName": "/MinimalApi",
"ActivityHttpRoute": "",
"MetricHttpRoute": "/MinimalApi",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/MinimalApi",
"RoutePattern.RawText": "/MinimalApi",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {},
"ActionDescriptor": null
}
}
```
## MinimalApi: Action with parameter
```json
{
"IdealHttpRoute": "/MinimalApi/{id}",
"ActivityDisplayName": "/MinimalApi/123",
"ActivityHttpRoute": "",
"MetricHttpRoute": "/MinimalApi/{id}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/MinimalApi/123",
"RoutePattern.RawText": "/MinimalApi/{id}",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"id": "123"
},
"ActionDescriptor": null
}
}
```
## MinimalApi: Action without parameter (MapGroup)
```json
{
"IdealHttpRoute": "/MinimalApiUsingMapGroup/",
"ActivityDisplayName": "/MinimalApiUsingMapGroup",
"ActivityHttpRoute": "",
"MetricHttpRoute": "/MinimalApiUsingMapGroup/",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/MinimalApiUsingMapGroup",
"RoutePattern.RawText": "/MinimalApiUsingMapGroup/",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {},
"ActionDescriptor": null
}
}
```
## MinimalApi: Action with parameter (MapGroup)
```json
{
"IdealHttpRoute": "/MinimalApiUsingMapGroup/{id}",
"ActivityDisplayName": "/MinimalApiUsingMapGroup/123",
"ActivityHttpRoute": "",
"MetricHttpRoute": "/MinimalApiUsingMapGroup/{id}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/MinimalApiUsingMapGroup/123",
"RoutePattern.RawText": "/MinimalApiUsingMapGroup/{id}",
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {
"id": "123"
},
"ActionDescriptor": null
}
}
```

View File

@ -0,0 +1,634 @@
# Test results for ASP.NET Core 8
| Span http.route | Metric http.route | App | Test Name |
| - | - | - | - |
| :broken_heart: | :broken_heart: | ConventionalRouting | [Root path](#conventionalrouting-root-path) |
| :broken_heart: | :broken_heart: | ConventionalRouting | [Non-default action with route parameter and query string](#conventionalrouting-non-default-action-with-route-parameter-and-query-string) |
| :broken_heart: | :broken_heart: | ConventionalRouting | [Non-default action with query string](#conventionalrouting-non-default-action-with-query-string) |
| :broken_heart: | :broken_heart: | ConventionalRouting | [Not Found (404)](#conventionalrouting-not-found-404) |
| :broken_heart: | :green_heart: | ConventionalRouting | [Route template with parameter constraint](#conventionalrouting-route-template-with-parameter-constraint) |
| :broken_heart: | :green_heart: | ConventionalRouting | [Path that does not match parameter constraint](#conventionalrouting-path-that-does-not-match-parameter-constraint) |
| :broken_heart: | :broken_heart: | ConventionalRouting | [Area using area:exists, default controller/action](#conventionalrouting-area-using-areaexists-default-controlleraction) |
| :broken_heart: | :broken_heart: | ConventionalRouting | [Area using area:exists, non-default action](#conventionalrouting-area-using-areaexists-non-default-action) |
| :broken_heart: | :broken_heart: | ConventionalRouting | [Area w/o area:exists, default controller/action](#conventionalrouting-area-wo-areaexists-default-controlleraction) |
| :green_heart: | :green_heart: | AttributeRouting | [Default action](#attributerouting-default-action) |
| :green_heart: | :green_heart: | AttributeRouting | [Action without parameter](#attributerouting-action-without-parameter) |
| :green_heart: | :green_heart: | AttributeRouting | [Action with parameter](#attributerouting-action-with-parameter) |
| :green_heart: | :green_heart: | AttributeRouting | [Action with parameter before action name in template](#attributerouting-action-with-parameter-before-action-name-in-template) |
| :green_heart: | :green_heart: | AttributeRouting | [Action invoked resulting in 400 Bad Request](#attributerouting-action-invoked-resulting-in-400-bad-request) |
| :broken_heart: | :broken_heart: | RazorPages | [Root path](#razorpages-root-path) |
| :broken_heart: | :broken_heart: | RazorPages | [Index page](#razorpages-index-page) |
| :broken_heart: | :broken_heart: | RazorPages | [Throws exception](#razorpages-throws-exception) |
| :green_heart: | :green_heart: | RazorPages | [Static content](#razorpages-static-content) |
| :broken_heart: | :green_heart: | MinimalApi | [Action without parameter](#minimalapi-action-without-parameter) |
| :broken_heart: | :green_heart: | MinimalApi | [Action with parameter](#minimalapi-action-with-parameter) |
| :broken_heart: | :green_heart: | MinimalApi | [Action without parameter (MapGroup)](#minimalapi-action-without-parameter-mapgroup) |
| :broken_heart: | :green_heart: | MinimalApi | [Action with parameter (MapGroup)](#minimalapi-action-with-parameter-mapgroup) |
## ConventionalRouting: Root path
```json
{
"IdealHttpRoute": "ConventionalRoute/Default/{id?}",
"ActivityDisplayName": "/",
"ActivityHttpRoute": "",
"MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/",
"RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"IRouteDiagnosticsMetadata.Route": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"HttpContext.GetRouteData()": {
"controller": "ConventionalRoute",
"action": "Default"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": null,
"Parameters": [],
"ControllerActionDescriptor": {
"ControllerName": "ConventionalRoute",
"ActionName": "Default"
},
"PageActionDescriptor": null
}
}
}
```
## ConventionalRouting: Non-default action with route parameter and query string
```json
{
"IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}",
"ActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter/2",
"ActivityHttpRoute": "",
"MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/ConventionalRoute/ActionWithStringParameter/2?num=3",
"RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"IRouteDiagnosticsMetadata.Route": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"HttpContext.GetRouteData()": {
"controller": "ConventionalRoute",
"action": "ActionWithStringParameter",
"id": "2"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": null,
"Parameters": [
"id",
"num"
],
"ControllerActionDescriptor": {
"ControllerName": "ConventionalRoute",
"ActionName": "ActionWithStringParameter"
},
"PageActionDescriptor": null
}
}
}
```
## ConventionalRouting: Non-default action with query string
```json
{
"IdealHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}",
"ActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter",
"ActivityHttpRoute": "",
"MetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/ConventionalRoute/ActionWithStringParameter?num=3",
"RoutePattern.RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"IRouteDiagnosticsMetadata.Route": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"HttpContext.GetRouteData()": {
"controller": "ConventionalRoute",
"action": "ActionWithStringParameter"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": null,
"Parameters": [
"id",
"num"
],
"ControllerActionDescriptor": {
"ControllerName": "ConventionalRoute",
"ActionName": "ActionWithStringParameter"
},
"PageActionDescriptor": null
}
}
}
```
## ConventionalRouting: Not Found (404)
```json
{
"IdealHttpRoute": "",
"ActivityDisplayName": "/ConventionalRoute/NotFound",
"ActivityHttpRoute": "",
"MetricHttpRoute": "",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/ConventionalRoute/NotFound",
"RoutePattern.RawText": null,
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {},
"ActionDescriptor": null
}
}
```
## ConventionalRouting: Route template with parameter constraint
```json
{
"IdealHttpRoute": "SomePath/{id}/{num:int}",
"ActivityDisplayName": "/SomePath/SomeString/2",
"ActivityHttpRoute": "",
"MetricHttpRoute": "SomePath/{id}/{num:int}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/SomePath/SomeString/2",
"RoutePattern.RawText": "SomePath/{id}/{num:int}",
"IRouteDiagnosticsMetadata.Route": "SomePath/{id}/{num:int}",
"HttpContext.GetRouteData()": {
"controller": "ConventionalRoute",
"action": "ActionWithStringParameter",
"id": "SomeString",
"num": "2"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": null,
"Parameters": [
"id",
"num"
],
"ControllerActionDescriptor": {
"ControllerName": "ConventionalRoute",
"ActionName": "ActionWithStringParameter"
},
"PageActionDescriptor": null
}
}
}
```
## ConventionalRouting: Path that does not match parameter constraint
```json
{
"IdealHttpRoute": "",
"ActivityDisplayName": "/SomePath/SomeString/NotAnInt",
"ActivityHttpRoute": "",
"MetricHttpRoute": "",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/SomePath/SomeString/NotAnInt",
"RoutePattern.RawText": null,
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {},
"ActionDescriptor": null
}
}
```
## ConventionalRouting: Area using area:exists, default controller/action
```json
{
"IdealHttpRoute": "{area:exists}/ControllerForMyArea/Default/{id?}",
"ActivityDisplayName": "/MyArea",
"ActivityHttpRoute": "",
"MetricHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/MyArea",
"RoutePattern.RawText": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
"IRouteDiagnosticsMetadata.Route": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
"HttpContext.GetRouteData()": {
"controller": "ControllerForMyArea",
"action": "Default",
"area": "MyArea"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": null,
"Parameters": [],
"ControllerActionDescriptor": {
"ControllerName": "ControllerForMyArea",
"ActionName": "Default"
},
"PageActionDescriptor": null
}
}
}
```
## ConventionalRouting: Area using area:exists, non-default action
```json
{
"IdealHttpRoute": "{area:exists}/ControllerForMyArea/NonDefault/{id?}",
"ActivityDisplayName": "/MyArea/ControllerForMyArea/NonDefault",
"ActivityHttpRoute": "",
"MetricHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/MyArea/ControllerForMyArea/NonDefault",
"RoutePattern.RawText": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
"IRouteDiagnosticsMetadata.Route": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
"HttpContext.GetRouteData()": {
"controller": "ControllerForMyArea",
"area": "MyArea",
"action": "NonDefault"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": null,
"Parameters": [],
"ControllerActionDescriptor": {
"ControllerName": "ControllerForMyArea",
"ActionName": "NonDefault"
},
"PageActionDescriptor": null
}
}
}
```
## ConventionalRouting: Area w/o area:exists, default controller/action
```json
{
"IdealHttpRoute": "SomePrefix/AnotherArea/Index/{id?}",
"ActivityDisplayName": "/SomePrefix",
"ActivityHttpRoute": "",
"MetricHttpRoute": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/SomePrefix",
"RoutePattern.RawText": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}",
"IRouteDiagnosticsMetadata.Route": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}",
"HttpContext.GetRouteData()": {
"area": "AnotherArea",
"controller": "AnotherArea",
"action": "Index"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": null,
"Parameters": [],
"ControllerActionDescriptor": {
"ControllerName": "AnotherArea",
"ActionName": "Index"
},
"PageActionDescriptor": null
}
}
}
```
## AttributeRouting: Default action
```json
{
"IdealHttpRoute": "AttributeRoute",
"ActivityDisplayName": "AttributeRoute",
"ActivityHttpRoute": "AttributeRoute",
"MetricHttpRoute": "AttributeRoute",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/AttributeRoute",
"RoutePattern.RawText": "AttributeRoute",
"IRouteDiagnosticsMetadata.Route": "AttributeRoute",
"HttpContext.GetRouteData()": {
"action": "Get",
"controller": "AttributeRoute"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": "AttributeRoute",
"Parameters": [],
"ControllerActionDescriptor": {
"ControllerName": "AttributeRoute",
"ActionName": "Get"
},
"PageActionDescriptor": null
}
}
}
```
## AttributeRouting: Action without parameter
```json
{
"IdealHttpRoute": "AttributeRoute/Get",
"ActivityDisplayName": "AttributeRoute/Get",
"ActivityHttpRoute": "AttributeRoute/Get",
"MetricHttpRoute": "AttributeRoute/Get",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/AttributeRoute/Get",
"RoutePattern.RawText": "AttributeRoute/Get",
"IRouteDiagnosticsMetadata.Route": "AttributeRoute/Get",
"HttpContext.GetRouteData()": {
"action": "Get",
"controller": "AttributeRoute"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": "AttributeRoute/Get",
"Parameters": [],
"ControllerActionDescriptor": {
"ControllerName": "AttributeRoute",
"ActionName": "Get"
},
"PageActionDescriptor": null
}
}
}
```
## AttributeRouting: Action with parameter
```json
{
"IdealHttpRoute": "AttributeRoute/Get/{id}",
"ActivityDisplayName": "AttributeRoute/Get/{id}",
"ActivityHttpRoute": "AttributeRoute/Get/{id}",
"MetricHttpRoute": "AttributeRoute/Get/{id}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/AttributeRoute/Get/12",
"RoutePattern.RawText": "AttributeRoute/Get/{id}",
"IRouteDiagnosticsMetadata.Route": "AttributeRoute/Get/{id}",
"HttpContext.GetRouteData()": {
"action": "Get",
"controller": "AttributeRoute",
"id": "12"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": "AttributeRoute/Get/{id}",
"Parameters": [
"id"
],
"ControllerActionDescriptor": {
"ControllerName": "AttributeRoute",
"ActionName": "Get"
},
"PageActionDescriptor": null
}
}
}
```
## AttributeRouting: Action with parameter before action name in template
```json
{
"IdealHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"ActivityDisplayName": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"ActivityHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"MetricHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/AttributeRoute/12/GetWithActionNameInDifferentSpotInTemplate",
"RoutePattern.RawText": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"IRouteDiagnosticsMetadata.Route": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"HttpContext.GetRouteData()": {
"action": "GetWithActionNameInDifferentSpotInTemplate",
"controller": "AttributeRoute",
"id": "12"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"Parameters": [
"id"
],
"ControllerActionDescriptor": {
"ControllerName": "AttributeRoute",
"ActionName": "GetWithActionNameInDifferentSpotInTemplate"
},
"PageActionDescriptor": null
}
}
}
```
## AttributeRouting: Action invoked resulting in 400 Bad Request
```json
{
"IdealHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"ActivityDisplayName": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"ActivityHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"MetricHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/AttributeRoute/NotAnInt/GetWithActionNameInDifferentSpotInTemplate",
"RoutePattern.RawText": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"IRouteDiagnosticsMetadata.Route": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"HttpContext.GetRouteData()": {
"action": "GetWithActionNameInDifferentSpotInTemplate",
"controller": "AttributeRoute",
"id": "NotAnInt"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"Parameters": [
"id"
],
"ControllerActionDescriptor": {
"ControllerName": "AttributeRoute",
"ActionName": "GetWithActionNameInDifferentSpotInTemplate"
},
"PageActionDescriptor": null
}
}
}
```
## RazorPages: Root path
```json
{
"IdealHttpRoute": "/Index",
"ActivityDisplayName": "/",
"ActivityHttpRoute": "",
"MetricHttpRoute": "",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/",
"RoutePattern.RawText": "",
"IRouteDiagnosticsMetadata.Route": "",
"HttpContext.GetRouteData()": {
"page": "/Index"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": "",
"Parameters": [],
"ControllerActionDescriptor": null,
"PageActionDescriptor": {
"RelativePath": "/Pages/Index.cshtml",
"ViewEnginePath": "/Index"
}
}
}
}
```
## RazorPages: Index page
```json
{
"IdealHttpRoute": "/Index",
"ActivityDisplayName": "Index",
"ActivityHttpRoute": "Index",
"MetricHttpRoute": "Index",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/Index",
"RoutePattern.RawText": "Index",
"IRouteDiagnosticsMetadata.Route": "Index",
"HttpContext.GetRouteData()": {
"page": "/Index"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": "Index",
"Parameters": [],
"ControllerActionDescriptor": null,
"PageActionDescriptor": {
"RelativePath": "/Pages/Index.cshtml",
"ViewEnginePath": "/Index"
}
}
}
}
```
## RazorPages: Throws exception
```json
{
"IdealHttpRoute": "/PageThatThrowsException",
"ActivityDisplayName": "PageThatThrowsException",
"ActivityHttpRoute": "PageThatThrowsException",
"MetricHttpRoute": "PageThatThrowsException",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/PageThatThrowsException",
"RoutePattern.RawText": "PageThatThrowsException",
"IRouteDiagnosticsMetadata.Route": "PageThatThrowsException",
"HttpContext.GetRouteData()": {
"page": "/PageThatThrowsException"
},
"ActionDescriptor": {
"AttributeRouteInfo.Template": "PageThatThrowsException",
"Parameters": [],
"ControllerActionDescriptor": null,
"PageActionDescriptor": {
"RelativePath": "/Pages/PageThatThrowsException.cshtml",
"ViewEnginePath": "/PageThatThrowsException"
}
}
}
}
```
## RazorPages: Static content
```json
{
"IdealHttpRoute": "",
"ActivityDisplayName": "/js/site.js",
"ActivityHttpRoute": "",
"MetricHttpRoute": "",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/js/site.js",
"RoutePattern.RawText": null,
"IRouteDiagnosticsMetadata.Route": null,
"HttpContext.GetRouteData()": {},
"ActionDescriptor": null
}
}
```
## MinimalApi: Action without parameter
```json
{
"IdealHttpRoute": "/MinimalApi",
"ActivityDisplayName": "/MinimalApi",
"ActivityHttpRoute": "",
"MetricHttpRoute": "/MinimalApi",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/MinimalApi",
"RoutePattern.RawText": "/MinimalApi",
"IRouteDiagnosticsMetadata.Route": "/MinimalApi",
"HttpContext.GetRouteData()": {},
"ActionDescriptor": null
}
}
```
## MinimalApi: Action with parameter
```json
{
"IdealHttpRoute": "/MinimalApi/{id}",
"ActivityDisplayName": "/MinimalApi/123",
"ActivityHttpRoute": "",
"MetricHttpRoute": "/MinimalApi/{id}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/MinimalApi/123",
"RoutePattern.RawText": "/MinimalApi/{id}",
"IRouteDiagnosticsMetadata.Route": "/MinimalApi/{id}",
"HttpContext.GetRouteData()": {
"id": "123"
},
"ActionDescriptor": null
}
}
```
## MinimalApi: Action without parameter (MapGroup)
```json
{
"IdealHttpRoute": "/MinimalApiUsingMapGroup/",
"ActivityDisplayName": "/MinimalApiUsingMapGroup",
"ActivityHttpRoute": "",
"MetricHttpRoute": "/MinimalApiUsingMapGroup/",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/MinimalApiUsingMapGroup",
"RoutePattern.RawText": "/MinimalApiUsingMapGroup/",
"IRouteDiagnosticsMetadata.Route": "/MinimalApiUsingMapGroup/",
"HttpContext.GetRouteData()": {},
"ActionDescriptor": null
}
}
```
## MinimalApi: Action with parameter (MapGroup)
```json
{
"IdealHttpRoute": "/MinimalApiUsingMapGroup/{id}",
"ActivityDisplayName": "/MinimalApiUsingMapGroup/123",
"ActivityHttpRoute": "",
"MetricHttpRoute": "/MinimalApiUsingMapGroup/{id}",
"RouteInfo": {
"HttpMethod": "GET",
"Path": "/MinimalApiUsingMapGroup/123",
"RoutePattern.RawText": "/MinimalApiUsingMapGroup/{id}",
"IRouteDiagnosticsMetadata.Route": "/MinimalApiUsingMapGroup/{id}",
"HttpContext.GetRouteData()": {
"id": "123"
},
"ActionDescriptor": null
}
}
```

View File

@ -0,0 +1,87 @@
// <copyright file="RoutingTestCases.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
#nullable enable
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using RouteTests.TestApplication;
namespace RouteTests;
public static class RoutingTestCases
{
public static IEnumerable<object[]> GetTestCases()
{
var assembly = Assembly.GetExecutingAssembly();
var input = JsonSerializer.Deserialize<TestCase[]>(
assembly.GetManifestResourceStream("RoutingTestCases.json")!,
new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = { new JsonStringEnumConverter() },
});
return GetArgumentsFromTestCaseObject(input!);
}
private static IEnumerable<object[]> GetArgumentsFromTestCaseObject(IEnumerable<TestCase> input)
{
var result = new List<object[]>();
foreach (var testCase in input)
{
if (testCase.MinimumDotnetVersion.HasValue && Environment.Version.Major < testCase.MinimumDotnetVersion.Value)
{
continue;
}
result.Add(new object[] { testCase, true });
result.Add(new object[] { testCase, false });
}
return result;
}
public class TestCase
{
public string Name { get; set; } = string.Empty;
public int? MinimumDotnetVersion { get; set; }
public TestApplicationScenario TestApplicationScenario { get; set; }
public string? HttpMethod { get; set; }
public string Path { get; set; } = string.Empty;
public int ExpectedStatusCode { get; set; }
public string? ExpectedHttpRoute { get; set; }
public string? CurrentActivityDisplayName { get; set; }
public string? CurrentActivityHttpRoute { get; set; }
public string? CurrentMetricHttpRoute { get; set; }
public override string ToString()
{
// This is used by Visual Studio's test runner to identify the test case.
return $"{this.TestApplicationScenario}: {this.Name}";
}
}
}

View File

@ -0,0 +1,246 @@
[
{
"name": "Root path",
"testApplicationScenario": "ConventionalRouting",
"httpMethod": "GET",
"path": "/",
"expectedStatusCode": 200,
"currentActivityDisplayName": "/",
"currentActivityHttpRoute": "",
"currentMetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"expectedHttpRoute": "ConventionalRoute/Default/{id?}"
},
{
"name": "Non-default action with route parameter and query string",
"testApplicationScenario": "ConventionalRouting",
"httpMethod": "GET",
"path": "/ConventionalRoute/ActionWithStringParameter/2?num=3",
"expectedStatusCode": 200,
"currentActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter/2",
"currentActivityHttpRoute": "",
"currentMetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"expectedHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}"
},
{
"name": "Non-default action with query string",
"testApplicationScenario": "ConventionalRouting",
"httpMethod": "GET",
"path": "/ConventionalRoute/ActionWithStringParameter?num=3",
"expectedStatusCode": 200,
"currentActivityDisplayName": "/ConventionalRoute/ActionWithStringParameter",
"currentActivityHttpRoute": "",
"currentMetricHttpRoute": "{controller=ConventionalRoute}/{action=Default}/{id?}",
"expectedHttpRoute": "ConventionalRoute/ActionWithStringParameter/{id?}"
},
{
"name": "Not Found (404)",
"testApplicationScenario": "ConventionalRouting",
"httpMethod": "GET",
"path": "/ConventionalRoute/NotFound",
"expectedStatusCode": 404,
"currentActivityDisplayName": "/ConventionalRoute/NotFound",
"currentActivityHttpRoute": "",
"currentMetricHttpRoute": "",
"expectedHttpRoute": ""
},
{
"name": "Route template with parameter constraint",
"testApplicationScenario": "ConventionalRouting",
"httpMethod": "GET",
"path": "/SomePath/SomeString/2",
"expectedStatusCode": 200,
"currentActivityDisplayName": "/SomePath/SomeString/2",
"currentActivityHttpRoute": "",
"currentMetricHttpRoute": null,
"expectedHttpRoute": "SomePath/{id}/{num:int}"
},
{
"name": "Path that does not match parameter constraint",
"testApplicationScenario": "ConventionalRouting",
"httpMethod": "GET",
"path": "/SomePath/SomeString/NotAnInt",
"expectedStatusCode": 404,
"currentActivityDisplayName": "/SomePath/SomeString/NotAnInt",
"currentActivityHttpRoute": "",
"currentMetricHttpRoute": null,
"expectedHttpRoute": ""
},
{
"name": "Area using area:exists, default controller/action",
"testApplicationScenario": "ConventionalRouting",
"httpMethod": "GET",
"path": "/MyArea",
"expectedStatusCode": 200,
"currentActivityDisplayName": "/MyArea",
"currentActivityHttpRoute": "",
"currentMetricHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
"expectedHttpRoute": "{area:exists}/ControllerForMyArea/Default/{id?}"
},
{
"name": "Area using area:exists, non-default action",
"testApplicationScenario": "ConventionalRouting",
"httpMethod": "GET",
"path": "/MyArea/ControllerForMyArea/NonDefault",
"expectedStatusCode": 200,
"currentActivityDisplayName": "/MyArea/ControllerForMyArea/NonDefault",
"currentActivityHttpRoute": "",
"currentMetricHttpRoute": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
"expectedHttpRoute": "{area:exists}/ControllerForMyArea/NonDefault/{id?}"
},
{
"name": "Area w/o area:exists, default controller/action",
"testApplicationScenario": "ConventionalRouting",
"httpMethod": "GET",
"path": "/SomePrefix",
"expectedStatusCode": 200,
"currentActivityDisplayName": "/SomePrefix",
"currentActivityHttpRoute": "",
"currentMetricHttpRoute": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}",
"expectedHttpRoute": "SomePrefix/AnotherArea/Index/{id?}"
},
{
"name": "Default action",
"testApplicationScenario": "AttributeRouting",
"httpMethod": "GET",
"path": "/AttributeRoute",
"expectedStatusCode": 200,
"currentActivityDisplayName": "AttributeRoute",
"currentActivityHttpRoute": null,
"currentMetricHttpRoute": null,
"expectedHttpRoute": "AttributeRoute"
},
{
"name": "Action without parameter",
"testApplicationScenario": "AttributeRouting",
"httpMethod": "GET",
"path": "/AttributeRoute/Get",
"expectedStatusCode": 200,
"currentActivityDisplayName": "AttributeRoute/Get",
"currentActivityHttpRoute": null,
"currentMetricHttpRoute": null,
"expectedHttpRoute": "AttributeRoute/Get"
},
{
"name": "Action with parameter",
"testApplicationScenario": "AttributeRouting",
"httpMethod": "GET",
"path": "/AttributeRoute/Get/12",
"expectedStatusCode": 200,
"currentActivityDisplayName": "AttributeRoute/Get/{id}",
"currentActivityHttpRoute": null,
"currentMetricHttpRoute": null,
"expectedHttpRoute": "AttributeRoute/Get/{id}"
},
{
"name": "Action with parameter before action name in template",
"testApplicationScenario": "AttributeRouting",
"httpMethod": "GET",
"path": "/AttributeRoute/12/GetWithActionNameInDifferentSpotInTemplate",
"expectedStatusCode": 200,
"currentActivityDisplayName": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"currentActivityHttpRoute": null,
"currentMetricHttpRoute": null,
"expectedHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate"
},
{
"name": "Action invoked resulting in 400 Bad Request",
"testApplicationScenario": "AttributeRouting",
"httpMethod": "GET",
"path": "/AttributeRoute/NotAnInt/GetWithActionNameInDifferentSpotInTemplate",
"expectedStatusCode": 400,
"currentActivityDisplayName": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
"currentActivityHttpRoute": null,
"currentMetricHttpRoute": null,
"expectedHttpRoute": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate"
},
{
"name": "Root path",
"testApplicationScenario": "RazorPages",
"httpMethod": "GET",
"path": "/",
"expectedStatusCode": 200,
"currentActivityDisplayName": "/",
"currentActivityHttpRoute": "",
"currentMetricHttpRoute": "",
"expectedHttpRoute": "/Index"
},
{
"name": "Index page",
"testApplicationScenario": "RazorPages",
"httpMethod": "GET",
"path": "/Index",
"expectedStatusCode": 200,
"currentActivityDisplayName": "Index",
"currentActivityHttpRoute": "Index",
"currentMetricHttpRoute": "Index",
"expectedHttpRoute": "/Index"
},
{
"name": "Throws exception",
"testApplicationScenario": "RazorPages",
"httpMethod": "GET",
"path": "/PageThatThrowsException",
"expectedStatusCode": 500,
"currentActivityDisplayName": "PageThatThrowsException",
"currentActivityHttpRoute": "PageThatThrowsException",
"currentMetricHttpRoute": "PageThatThrowsException",
"expectedHttpRoute": "/PageThatThrowsException"
},
{
"name": "Static content",
"testApplicationScenario": "RazorPages",
"httpMethod": "GET",
"path": "/js/site.js",
"expectedStatusCode": 200,
"currentActivityDisplayName": "/js/site.js",
"currentActivityHttpRoute": null,
"currentMetricHttpRoute": null,
"expectedHttpRoute": ""
},
{
"name": "Action without parameter",
"testApplicationScenario": "MinimalApi",
"httpMethod": "GET",
"path": "/MinimalApi",
"expectedStatusCode": 200,
"currentActivityDisplayName": "/MinimalApi",
"currentActivityHttpRoute": "",
"currentMetricHttpRoute": null,
"expectedHttpRoute": "/MinimalApi"
},
{
"name": "Action with parameter",
"testApplicationScenario": "MinimalApi",
"httpMethod": "GET",
"path": "/MinimalApi/123",
"expectedStatusCode": 200,
"currentActivityDisplayName": "/MinimalApi/123",
"currentActivityHttpRoute": "",
"currentMetricHttpRoute": null,
"expectedHttpRoute": "/MinimalApi/{id}"
},
{
"name": "Action without parameter (MapGroup)",
"minimumDotnetVersion": 7,
"testApplicationScenario": "MinimalApi",
"httpMethod": "GET",
"path": "/MinimalApiUsingMapGroup",
"expectedStatusCode": 200,
"currentActivityDisplayName": "/MinimalApiUsingMapGroup",
"currentActivityHttpRoute": "",
"currentMetricHttpRoute": null,
"expectedHttpRoute": "/MinimalApiUsingMapGroup/"
},
{
"name": "Action with parameter (MapGroup)",
"minimumDotnetVersion": 7,
"testApplicationScenario": "MinimalApi",
"httpMethod": "GET",
"path": "/MinimalApiUsingMapGroup/123",
"expectedStatusCode": 200,
"currentActivityDisplayName": "/MinimalApiUsingMapGroup/123",
"currentActivityHttpRoute": "",
"currentMetricHttpRoute": null,
"expectedHttpRoute": "/MinimalApiUsingMapGroup/{id}"
}
]

View File

@ -0,0 +1,121 @@
// <copyright file="RoutingTestFixture.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
#nullable enable
using System.Text;
using Microsoft.AspNetCore.Builder;
using RouteTests.TestApplication;
namespace RouteTests;
public class RoutingTestFixture : IDisposable
{
private static readonly HttpClient HttpClient = new();
private readonly Dictionary<TestApplicationScenario, WebApplication> apps = new();
private readonly RouteInfoDiagnosticObserver diagnostics = new();
private readonly List<RoutingTestResult> testResults = new();
public RoutingTestFixture()
{
foreach (var scenario in Enum.GetValues<TestApplicationScenario>())
{
var app = TestApplicationFactory.CreateApplication(scenario);
if (app != null)
{
this.apps.Add(scenario, app);
}
}
foreach (var app in this.apps)
{
app.Value.RunAsync();
}
}
public async Task MakeRequest(TestApplicationScenario scenario, string path)
{
var app = this.apps[scenario];
var baseUrl = app.Urls.First();
var url = $"{baseUrl}{path}";
await HttpClient.GetAsync(url).ConfigureAwait(false);
}
public void AddTestResult(RoutingTestResult result)
{
this.testResults.Add(result);
}
public void Dispose()
{
foreach (var app in this.apps)
{
app.Value.DisposeAsync().GetAwaiter().GetResult();
}
HttpClient.Dispose();
this.diagnostics.Dispose();
this.GenerateReadme();
}
private void GenerateReadme()
{
var sb = new StringBuilder();
sb.AppendLine($"# Test results for ASP.NET Core {Environment.Version.Major}");
sb.AppendLine();
sb.AppendLine("| Span http.route | Metric http.route | App | Test Name |");
sb.AppendLine("| - | - | - | - |");
for (var i = 0; i < this.testResults.Count; ++i)
{
var result = this.testResults[i];
var emoji1 = result.TestCase.CurrentActivityHttpRoute == null ? ":green_heart:" : ":broken_heart:";
var emoji2 = result.TestCase.CurrentMetricHttpRoute == null ? ":green_heart:" : ":broken_heart:";
sb.Append($"| {emoji1} | {emoji2} ");
sb.AppendLine($"| {result.TestCase.TestApplicationScenario} | [{result.TestCase.Name}]({MakeAnchorTag(result.TestCase.TestApplicationScenario, result.TestCase.Name)}) |");
}
for (var i = 0; i < this.testResults.Count; ++i)
{
var result = this.testResults[i];
sb.AppendLine();
sb.AppendLine($"## {result.TestCase.TestApplicationScenario}: {result.TestCase.Name}");
sb.AppendLine();
sb.AppendLine("```json");
sb.AppendLine(result.ToString());
sb.AppendLine("```");
}
var readmeFileName = $"README.net{Environment.Version.Major}.0.md";
File.WriteAllText(Path.Combine("..", "..", "..", "RouteTests", readmeFileName), sb.ToString());
static string MakeAnchorTag(TestApplicationScenario scenario, string name)
{
var chars = name.ToCharArray()
.Where(c => !char.IsPunctuation(c) || c == '-')
.Select(c => c switch
{
'-' => '-',
' ' => '-',
_ => char.ToLower(c),
})
.ToArray();
return $"#{scenario.ToString().ToLower()}-{new string(chars)}";
}
}
}

View File

@ -0,0 +1,46 @@
// <copyright file="RoutingTestResult.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
#nullable enable
using System.Text.Json;
using System.Text.Json.Serialization;
using RouteTests.TestApplication;
namespace RouteTests;
public class RoutingTestResult
{
private static readonly JsonSerializerOptions JsonSerializerOptions = new() { WriteIndented = true };
public string? IdealHttpRoute { get; set; }
public string ActivityDisplayName { get; set; } = string.Empty;
public string? ActivityHttpRoute { get; set; }
public string? MetricHttpRoute { get; set; }
public RouteInfo RouteInfo { get; set; } = new RouteInfo();
[JsonIgnore]
public RoutingTestCases.TestCase TestCase { get; set; } = new RoutingTestCases.TestCase();
public override string ToString()
{
return JsonSerializer.Serialize(this, JsonSerializerOptions);
}
}

View File

@ -0,0 +1,191 @@
// <copyright file="RoutingTests.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
#nullable enable
using System.Diagnostics;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
using RouteTests.TestApplication;
using Xunit;
using static OpenTelemetry.Internal.HttpSemanticConventionHelper;
namespace RouteTests;
public class RoutingTests : IClassFixture<RoutingTestFixture>
{
private const string OldHttpStatusCode = "http.status_code";
private const string OldHttpMethod = "http.method";
private const string HttpStatusCode = "http.response.status_code";
private const string HttpMethod = "http.request.method";
private const string HttpRoute = "http.route";
private readonly RoutingTestFixture fixture;
private readonly List<Activity> exportedActivities = new();
private readonly List<Metric> exportedMetrics = new();
public RoutingTests(RoutingTestFixture fixture)
{
this.fixture = fixture;
}
public static IEnumerable<object[]> TestData => RoutingTestCases.GetTestCases();
[Theory]
[MemberData(nameof(TestData))]
public async Task TestHttpRoute(RoutingTestCases.TestCase testCase, bool useLegacyConventions)
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?> { [SemanticConventionOptInKeyName] = useLegacyConventions ? null : "http" })
.Build();
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.ConfigureServices(services => services.AddSingleton<IConfiguration>(configuration))
.AddAspNetCoreInstrumentation()
.AddInMemoryExporter(this.exportedActivities)
.Build()!;
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureServices(services => services.AddSingleton<IConfiguration>(configuration))
.AddAspNetCoreInstrumentation()
.AddInMemoryExporter(this.exportedMetrics)
.Build()!;
await this.fixture.MakeRequest(testCase.TestApplicationScenario, testCase.Path);
for (var i = 0; i < 10; i++)
{
if (this.exportedActivities.Count > 0)
{
break;
}
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
}
meterProvider.ForceFlush();
var durationMetric = this.exportedMetrics.Single(x => x.Name == "http.server.request.duration" || x.Name == "http.server.duration");
var metricPoints = new List<MetricPoint>();
foreach (var mp in durationMetric.GetMetricPoints())
{
metricPoints.Add(mp);
}
var activity = Assert.Single(this.exportedActivities);
var metricPoint = Assert.Single(metricPoints);
GetTagsFromActivity(useLegacyConventions, activity, out var activityHttpStatusCode, out var activityHttpMethod, out var activityHttpRoute);
GetTagsFromMetricPoint(useLegacyConventions && Environment.Version.Major < 8, metricPoint, out var metricHttpStatusCode, out var metricHttpMethod, out var metricHttpRoute);
Assert.Equal(testCase.ExpectedStatusCode, activityHttpStatusCode);
Assert.Equal(testCase.ExpectedStatusCode, metricHttpStatusCode);
Assert.Equal(testCase.HttpMethod, activityHttpMethod);
Assert.Equal(testCase.HttpMethod, metricHttpMethod);
// TODO: The CurrentActivityDisplayName, CurrentActivityHttpRoute, and CurrentMetricHttpRoute
// properties will go away. They only serve to capture status quo. The "else" blocks are the real
// asserts that we ultimately want.
// If any of the current properties are null, then that means we already conform to the
// correct behavior.
if (testCase.CurrentActivityDisplayName != null)
{
Assert.Equal(testCase.CurrentActivityDisplayName, activity.DisplayName);
}
else
{
// Activity.DisplayName should be a combination of http.method + http.route attributes, see:
// https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#name
var expectedActivityDisplayName = string.IsNullOrEmpty(testCase.ExpectedHttpRoute)
? testCase.HttpMethod
: $"{testCase.HttpMethod} {testCase.ExpectedHttpRoute}";
Assert.Equal(expectedActivityDisplayName, activity.DisplayName);
}
if (testCase.CurrentActivityHttpRoute != null)
{
Assert.Equal(testCase.CurrentActivityHttpRoute, activityHttpRoute);
}
else
{
Assert.Equal(testCase.ExpectedHttpRoute, activityHttpRoute);
}
if (testCase.CurrentMetricHttpRoute != null)
{
Assert.Equal(testCase.CurrentMetricHttpRoute, metricHttpRoute);
}
else
{
Assert.Equal(testCase.ExpectedHttpRoute, metricHttpRoute);
}
// Only produce README files based on final semantic conventions
if (!useLegacyConventions)
{
var testResult = new RoutingTestResult
{
IdealHttpRoute = testCase.ExpectedHttpRoute,
ActivityDisplayName = activity.DisplayName,
ActivityHttpRoute = activityHttpRoute,
MetricHttpRoute = metricHttpRoute,
TestCase = testCase,
RouteInfo = RouteInfo.Current,
};
this.fixture.AddTestResult(testResult);
}
}
private static void GetTagsFromActivity(bool useLegacyConventions, Activity activity, out int httpStatusCode, out string httpMethod, out string? httpRoute)
{
var expectedStatusCodeKey = useLegacyConventions ? OldHttpStatusCode : HttpStatusCode;
var expectedHttpMethodKey = useLegacyConventions ? OldHttpMethod : HttpMethod;
httpStatusCode = Convert.ToInt32(activity.GetTagItem(expectedStatusCodeKey));
httpMethod = (activity.GetTagItem(expectedHttpMethodKey) as string)!;
httpRoute = activity.GetTagItem(HttpRoute) as string ?? string.Empty;
}
private static void GetTagsFromMetricPoint(bool useLegacyConventions, MetricPoint metricPoint, out int httpStatusCode, out string httpMethod, out string? httpRoute)
{
var expectedStatusCodeKey = useLegacyConventions ? OldHttpStatusCode : HttpStatusCode;
var expectedHttpMethodKey = useLegacyConventions ? OldHttpMethod : HttpMethod;
httpStatusCode = 0;
httpMethod = string.Empty;
httpRoute = string.Empty;
foreach (var tag in metricPoint.Tags)
{
if (tag.Key.Equals(expectedStatusCodeKey))
{
httpStatusCode = Convert.ToInt32(tag.Value);
}
else if (tag.Key.Equals(expectedHttpMethodKey))
{
httpMethod = (tag.Value as string)!;
}
else if (tag.Key.Equals(HttpRoute))
{
httpRoute = tag.Value as string;
}
}
}
}

View File

@ -0,0 +1,27 @@
// <copyright file="AnotherAreaController.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
#nullable disable
using Microsoft.AspNetCore.Mvc;
namespace RouteTests.Controllers;
[Area("AnotherArea")]
public class AnotherAreaController : Controller
{
public IActionResult Index() => this.Ok();
}

View File

@ -0,0 +1,29 @@
// <copyright file="ControllerForMyAreaController.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
#nullable disable
using Microsoft.AspNetCore.Mvc;
namespace RouteTests.Controllers;
[Area("MyArea")]
public class ControllerForMyAreaController : Controller
{
public IActionResult Default() => this.Ok();
public IActionResult NonDefault() => this.Ok();
}

View File

@ -0,0 +1,36 @@
// <copyright file="AttributeRouteController.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
#nullable disable
using Microsoft.AspNetCore.Mvc;
namespace RouteTests.Controllers;
[ApiController]
[Route("[controller]")]
public class AttributeRouteController : ControllerBase
{
[HttpGet]
[HttpGet("[action]")]
public IActionResult Get() => this.Ok();
[HttpGet("[action]/{id}")]
public IActionResult Get(int id) => this.Ok();
[HttpGet("{id}/[action]")]
public IActionResult GetWithActionNameInDifferentSpotInTemplate(int id) => this.Ok();
}

View File

@ -0,0 +1,30 @@
// <copyright file="ConventionalRouteController.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
#nullable disable
using Microsoft.AspNetCore.Mvc;
namespace RouteTests.Controllers;
public class ConventionalRouteController : Controller
{
public IActionResult Default() => this.Ok();
public IActionResult ActionWithParameter(int id) => this.Ok();
public IActionResult ActionWithStringParameter(string id, int num) => this.Ok();
}

View File

@ -0,0 +1,2 @@
@page
Hello, OpenTelemetry!

View File

@ -0,0 +1,4 @@
@page
@{
throw new Exception("Oops.");
}

View File

@ -0,0 +1,151 @@
// <copyright file="RouteInfo.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
#nullable enable
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Http;
#if NET8_0_OR_GREATER
using Microsoft.AspNetCore.Http.Metadata;
#endif
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Routing;
namespace RouteTests.TestApplication;
public class RouteInfo
{
public static RouteInfo Current { get; set; } = new();
public string? HttpMethod { get; set; }
public string? Path { get; set; }
[JsonPropertyName("RoutePattern.RawText")]
public string? RawText { get; set; }
[JsonPropertyName("IRouteDiagnosticsMetadata.Route")]
public string? RouteDiagnosticMetadata { get; set; }
[JsonPropertyName("HttpContext.GetRouteData()")]
public IDictionary<string, string?>? RouteData { get; set; }
public ActionDescriptorInfo? ActionDescriptor { get; set; }
public void SetValues(HttpContext context)
{
this.HttpMethod = context.Request.Method;
this.Path = $"{context.Request.Path}{context.Request.QueryString}";
var endpoint = context.GetEndpoint();
this.RawText = (endpoint as RouteEndpoint)?.RoutePattern.RawText;
#if NET8_0_OR_GREATER
this.RouteDiagnosticMetadata = endpoint?.Metadata.GetMetadata<IRouteDiagnosticsMetadata>()?.Route;
#endif
this.RouteData = new Dictionary<string, string?>();
foreach (var value in context.GetRouteData().Values)
{
this.RouteData[value.Key] = value.Value?.ToString();
}
}
public void SetValues(ActionDescriptor actionDescriptor)
{
if (this.ActionDescriptor == null)
{
this.ActionDescriptor = new ActionDescriptorInfo(actionDescriptor);
}
}
public class ActionDescriptorInfo
{
public ActionDescriptorInfo()
{
}
public ActionDescriptorInfo(ActionDescriptor actionDescriptor)
{
this.AttributeRouteInfo = actionDescriptor.AttributeRouteInfo?.Template;
this.ActionParameters = new List<string>();
foreach (var item in actionDescriptor.Parameters)
{
this.ActionParameters.Add(item.Name);
}
if (actionDescriptor is PageActionDescriptor pad)
{
this.PageActionDescriptorSummary = new PageActionDescriptorInfo(pad.RelativePath, pad.ViewEnginePath);
}
if (actionDescriptor is ControllerActionDescriptor cad)
{
this.ControllerActionDescriptorSummary = new ControllerActionDescriptorInfo(cad.ControllerName, cad.ActionName);
}
}
[JsonPropertyName("AttributeRouteInfo.Template")]
public string? AttributeRouteInfo { get; set; }
[JsonPropertyName("Parameters")]
public IList<string>? ActionParameters { get; set; }
[JsonPropertyName("ControllerActionDescriptor")]
public ControllerActionDescriptorInfo? ControllerActionDescriptorSummary { get; set; }
[JsonPropertyName("PageActionDescriptor")]
public PageActionDescriptorInfo? PageActionDescriptorSummary { get; set; }
}
public class ControllerActionDescriptorInfo
{
public ControllerActionDescriptorInfo()
{
}
public ControllerActionDescriptorInfo(string controllerName, string actionName)
{
this.ControllerActionDescriptorControllerName = controllerName;
this.ControllerActionDescriptorActionName = actionName;
}
[JsonPropertyName("ControllerName")]
public string ControllerActionDescriptorControllerName { get; set; } = string.Empty;
[JsonPropertyName("ActionName")]
public string ControllerActionDescriptorActionName { get; set; } = string.Empty;
}
public class PageActionDescriptorInfo
{
public PageActionDescriptorInfo()
{
}
public PageActionDescriptorInfo(string relativePath, string viewEnginePath)
{
this.PageActionDescriptorRelativePath = relativePath;
this.PageActionDescriptorViewEnginePath = viewEnginePath;
}
[JsonPropertyName("RelativePath")]
public string PageActionDescriptorRelativePath { get; set; } = string.Empty;
[JsonPropertyName("ViewEnginePath")]
public string PageActionDescriptorViewEnginePath { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,123 @@
// <copyright file="RouteInfoDiagnosticObserver.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
#nullable enable
using System.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Diagnostics;
namespace RouteTests.TestApplication;
/// <summary>
/// This observer captures all the available route information for a request.
/// This route information is used for generating a README file for analyzing
/// what information is available in different scenarios.
/// </summary>
internal sealed class RouteInfoDiagnosticObserver : IDisposable, IObserver<DiagnosticListener>, IObserver<KeyValuePair<string, object?>>
{
internal const string OnStartEvent = "Microsoft.AspNetCore.Hosting.HttpRequestIn.Start";
internal const string OnStopEvent = "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop";
internal const string OnMvcBeforeActionEvent = "Microsoft.AspNetCore.Mvc.BeforeAction";
private readonly List<IDisposable> listenerSubscriptions = new();
private IDisposable? allSourcesSubscription;
private long disposed;
public RouteInfoDiagnosticObserver()
{
this.allSourcesSubscription = DiagnosticListener.AllListeners.Subscribe(this);
}
public void OnNext(DiagnosticListener value)
{
if (value.Name == "Microsoft.AspNetCore")
{
var subscription = value.Subscribe(this);
lock (this.listenerSubscriptions)
{
this.listenerSubscriptions.Add(subscription);
}
}
}
public void OnNext(KeyValuePair<string, object?> value)
{
HttpContext? context;
BeforeActionEventData? actionMethodEventData;
RouteInfo? info;
switch (value.Key)
{
case OnStartEvent:
context = value.Value as HttpContext;
Debug.Assert(context != null, "HttpContext was null");
info = new RouteInfo();
info.SetValues(context);
RouteInfo.Current = info;
break;
case OnMvcBeforeActionEvent:
actionMethodEventData = value.Value as BeforeActionEventData;
Debug.Assert(actionMethodEventData != null, $"expected {nameof(BeforeActionEventData)}");
RouteInfo.Current.SetValues(actionMethodEventData.HttpContext);
RouteInfo.Current.SetValues(actionMethodEventData.ActionDescriptor);
break;
case OnStopEvent:
context = value.Value as HttpContext;
Debug.Assert(context != null, "HttpContext was null");
RouteInfo.Current.SetValues(context);
break;
default:
break;
}
}
public void OnCompleted()
{
}
public void OnError(Exception error)
{
}
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (Interlocked.CompareExchange(ref this.disposed, 1, 0) == 1)
{
return;
}
lock (this.listenerSubscriptions)
{
foreach (var listenerSubscription in this.listenerSubscriptions)
{
listenerSubscription?.Dispose();
}
this.listenerSubscriptions.Clear();
}
this.allSourcesSubscription?.Dispose();
this.allSourcesSubscription = null;
}
}

View File

@ -0,0 +1,170 @@
// <copyright file="TestApplicationFactory.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
#nullable enable
using System.Diagnostics;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
namespace RouteTests.TestApplication;
public enum TestApplicationScenario
{
/// <summary>
/// An application that uses conventional routing.
/// </summary>
ConventionalRouting,
/// <summary>
/// An application that uses attribute routing.
/// </summary>
AttributeRouting,
/// <summary>
/// A Minimal API application.
/// </summary>
MinimalApi,
/// <summary>
/// An Razor Pages application.
/// </summary>
RazorPages,
}
internal class TestApplicationFactory
{
private static readonly string AspNetCoreTestsPath = new FileInfo(typeof(RoutingTests)!.Assembly!.Location)!.Directory!.Parent!.Parent!.Parent!.FullName;
private static readonly string ContentRootPath = Path.Combine(AspNetCoreTestsPath, "RouteTests", "TestApplication");
public static WebApplication? CreateApplication(TestApplicationScenario config)
{
Debug.Assert(Directory.Exists(ContentRootPath), $"Cannot find ContentRootPath: {ContentRootPath}");
switch (config)
{
case TestApplicationScenario.ConventionalRouting:
return CreateConventionalRoutingApplication();
case TestApplicationScenario.AttributeRouting:
return CreateAttributeRoutingApplication();
case TestApplicationScenario.MinimalApi:
return CreateMinimalApiApplication();
case TestApplicationScenario.RazorPages:
return CreateRazorPagesApplication();
default:
throw new ArgumentException($"Invalid {nameof(TestApplicationScenario)}");
}
}
private static WebApplication CreateConventionalRoutingApplication()
{
var builder = WebApplication.CreateBuilder(new WebApplicationOptions { ContentRootPath = ContentRootPath });
builder.Logging.ClearProviders();
builder.Services
.AddControllersWithViews()
.AddApplicationPart(typeof(RoutingTests).Assembly);
var app = builder.Build();
app.Urls.Clear();
app.Urls.Add("http://[::1]:0");
app.UseStaticFiles();
app.UseRouting();
app.MapAreaControllerRoute(
name: "AnotherArea",
areaName: "AnotherArea",
pattern: "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}");
app.MapControllerRoute(
name: "MyArea",
pattern: "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}");
app.MapControllerRoute(
name: "FixedRouteWithConstraints",
pattern: "SomePath/{id}/{num:int}",
defaults: new { controller = "ConventionalRoute", action = "ActionWithStringParameter" });
app.MapControllerRoute(
name: "default",
pattern: "{controller=ConventionalRoute}/{action=Default}/{id?}");
return app;
}
private static WebApplication CreateAttributeRoutingApplication()
{
var builder = WebApplication.CreateBuilder();
builder.Logging.ClearProviders();
builder.Services
.AddControllers()
.AddApplicationPart(typeof(RoutingTests).Assembly);
var app = builder.Build();
app.Urls.Clear();
app.Urls.Add("http://[::1]:0");
app.MapControllers();
return app;
}
private static WebApplication CreateMinimalApiApplication()
{
var builder = WebApplication.CreateBuilder();
builder.Logging.ClearProviders();
var app = builder.Build();
app.Urls.Clear();
app.Urls.Add("http://[::1]:0");
app.MapGet("/MinimalApi", () => Results.Ok());
app.MapGet("/MinimalApi/{id}", (int id) => Results.Ok());
#if NET7_0_OR_GREATER
var api = app.MapGroup("/MinimalApiUsingMapGroup");
api.MapGet("/", () => Results.Ok());
api.MapGet("/{id}", (int id) => Results.Ok());
#endif
return app;
}
private static WebApplication CreateRazorPagesApplication()
{
var builder = WebApplication.CreateBuilder(new WebApplicationOptions { ContentRootPath = ContentRootPath });
builder.Logging.ClearProviders();
builder.Services
.AddRazorPages()
.AddRazorRuntimeCompilation(options =>
{
options.FileProviders.Add(new PhysicalFileProvider(ContentRootPath));
})
.AddApplicationPart(typeof(RoutingTests).Assembly);
var app = builder.Build();
app.Urls.Clear();
app.Urls.Add("http://[::1]:0");
app.UseStaticFiles();
app.UseRouting();
app.MapRazorPages();
return app;
}
}

View File

@ -0,0 +1,4 @@
// Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
// for details on configuring this project to bundle and minify static web assets.
// Write your JavaScript code.