Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 32 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
26fdf70
Add test suite to analyze ASP.NET Core routing scenarios
alanwest Oct 14, 2023
624f9fa
Generate readme after running test
alanwest Oct 24, 2023
9b2e211
Merge branch 'main' into alanwest/aspnetcore-routing
alanwest Oct 24, 2023
69c3289
Update expected values for minimal api
alanwest Oct 24, 2023
7d552b3
Fix tests
alanwest Oct 25, 2023
2f4c02c
Merge branch 'alanwest/aspnetcore-routing' of github.com:alanwest/ope…
alanwest Oct 25, 2023
2dfb64f
Readme for each version
alanwest Oct 25, 2023
6703455
Update testcases to assert current values
alanwest Oct 31, 2023
7501c97
Misc cleanup
alanwest Oct 31, 2023
85ca9db
Misc cleanup
alanwest Oct 31, 2023
dd71cc3
Merge branch 'main' into alanwest/aspnetcore-routing
alanwest Nov 1, 2023
afec0f6
Give test cases names and adjust output
alanwest Nov 2, 2023
a4bc6b2
Shorten test name
alanwest Nov 2, 2023
cfaae1b
Fix line endings
alanwest Nov 2, 2023
d501cd7
Merge branch 'main' into alanwest/aspnetcore-routing
alanwest Nov 2, 2023
2caf83c
Reorganize files
alanwest Nov 2, 2023
7a0fa17
Simplify capturing route information
alanwest Nov 3, 2023
e695361
Add readme
alanwest Nov 3, 2023
a994d52
Update readme
alanwest Nov 3, 2023
bcb7990
Remove links
alanwest Nov 3, 2023
f70af00
Merge branch 'main' into alanwest/aspnetcore-routing
alanwest Nov 3, 2023
ee84a96
:shrug: I guess docfx has some bugs in validating links.
alanwest Nov 3, 2023
3352598
Merge branch 'main' into alanwest/aspnetcore-routing
alanwest Nov 3, 2023
4231a39
Merge branch 'main' into alanwest/aspnetcore-routing
alanwest Nov 6, 2023
b30a1c4
PR feedback
alanwest Nov 7, 2023
830d3ef
Assert.Single
alanwest Nov 9, 2023
49b2f72
Test Minimal API on .NET 6
alanwest Nov 9, 2023
b101296
Make stuff static
alanwest Nov 9, 2023
8c0957e
Revert "Test Minimal API on .NET 6"
alanwest Nov 9, 2023
a335ec6
Test MapGroup for .NET 7+
alanwest Nov 9, 2023
79a4f53
Logging.ClearProviders()
alanwest Nov 9, 2023
5f120ff
Merge branch 'main' into alanwest/aspnetcore-routing
alanwest Nov 9, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down Expand Up @@ -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>
Original file line number Diff line number Diff line change
@@ -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
```
Loading