diff --git a/Directory.Packages.props b/Directory.Packages.props
index a2245fd9315..ff2112729a5 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -95,13 +95,16 @@
+
+
+
diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj
index f7400d295f8..214c232d21a 100644
--- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj
@@ -7,6 +7,7 @@
+
@@ -36,4 +37,11 @@
+
+
+
+ RouteTests.testcases.json
+ Always
+
+
diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/Areas/AnotherArea/Controllers/AnotherAreaController.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/Areas/AnotherArea/Controllers/AnotherAreaController.cs
new file mode 100644
index 00000000000..e9a2565304c
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/Areas/AnotherArea/Controllers/AnotherAreaController.cs
@@ -0,0 +1,27 @@
+//
+// 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.
+//
+
+#nullable disable
+
+using Microsoft.AspNetCore.Mvc;
+
+namespace RouteTests.Controllers;
+
+[Area("AnotherArea")]
+public class AnotherAreaController : Controller
+{
+ public IActionResult Index() => this.Ok();
+}
diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/Areas/MyArea/Controllers/ControllerForMyAreaController.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/Areas/MyArea/Controllers/ControllerForMyAreaController.cs
new file mode 100644
index 00000000000..99933dd34cf
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/Areas/MyArea/Controllers/ControllerForMyAreaController.cs
@@ -0,0 +1,29 @@
+//
+// 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.
+//
+
+#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();
+}
diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/AspNetCoreDiagnosticObserver.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/AspNetCoreDiagnosticObserver.cs
new file mode 100644
index 00000000000..de560214dff
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/AspNetCoreDiagnosticObserver.cs
@@ -0,0 +1,119 @@
+//
+// 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.
+//
+
+#nullable enable
+
+using System.Diagnostics;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Diagnostics;
+
+namespace RouteTests;
+
+internal sealed class AspNetCoreDiagnosticObserver : IDisposable, IObserver, IObserver>
+{
+ 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 listenerSubscriptions;
+ private IDisposable? allSourcesSubscription;
+ private long disposed;
+
+ public AspNetCoreDiagnosticObserver()
+ {
+ this.listenerSubscriptions = new List();
+ 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 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);
+ context.Items["RouteInfo"] = info;
+ break;
+ case OnMvcBeforeActionEvent:
+ actionMethodEventData = value.Value as BeforeActionEventData;
+ Debug.Assert(actionMethodEventData != null, $"expected {nameof(BeforeActionEventData)}");
+ info = actionMethodEventData.HttpContext.Items["RouteInfo"] as RouteInfo;
+ Debug.Assert(info != null, "RouteInfo object not present in context.Items");
+ info.SetValues(actionMethodEventData.HttpContext);
+ info.SetValues(actionMethodEventData.ActionDescriptor);
+ break;
+ case OnStopEvent:
+ // Can't update RouteInfo here because the response is already written.
+ 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;
+ }
+}
diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/Controllers/AttributeRouteController.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/Controllers/AttributeRouteController.cs
new file mode 100644
index 00000000000..28271f9553e
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/Controllers/AttributeRouteController.cs
@@ -0,0 +1,36 @@
+//
+// 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.
+//
+
+#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();
+}
diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/Controllers/ConventionalRouteController.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/Controllers/ConventionalRouteController.cs
new file mode 100644
index 00000000000..8dedc92cf04
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/Controllers/ConventionalRouteController.cs
@@ -0,0 +1,30 @@
+//
+// 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.
+//
+
+#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();
+}
diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/DebugInfo.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/DebugInfo.cs
new file mode 100644
index 00000000000..d60f07bc73a
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/DebugInfo.cs
@@ -0,0 +1,40 @@
+//
+// 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.
+//
+
+#nullable enable
+
+namespace RouteTests;
+
+public class DebugInfo
+{
+ public string? RawText { get; set; }
+
+ public string? RouteDiagnosticMetadata { get; set; }
+
+ public IDictionary? RouteData { get; set; }
+
+ public string? AttributeRouteInfo { get; set; }
+
+ public IList? ActionParameters { get; set; }
+
+ public string? PageActionDescriptorRelativePath { get; set; }
+
+ public string? PageActionDescriptorViewEnginePath { get; set; }
+
+ public string? ControllerActionDescriptorControllerName { get; set; }
+
+ public string? ControllerActionDescriptorActionName { get; set; }
+}
diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/Pages/Index.cshtml b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/Pages/Index.cshtml
new file mode 100644
index 00000000000..2ef32804b3b
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/Pages/Index.cshtml
@@ -0,0 +1,2 @@
+@page
+Hello, OpenTelemetry!
diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/Pages/PageThatThrowsException.cshtml b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/Pages/PageThatThrowsException.cshtml
new file mode 100644
index 00000000000..e95a1f02393
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/Pages/PageThatThrowsException.cshtml
@@ -0,0 +1,4 @@
+@page
+@{
+ throw new Exception("Oops.");
+}
diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.md b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.md
new file mode 100644
index 00000000000..b06ff12a4c4
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/README.md
@@ -0,0 +1,548 @@
+| | | display name | expected name (w/o http.method) | routing type | request |
+| - | - | - | - | - | - |
+| :broken_heart: | [1](#1) | / | ConventionalRoute/Default/{id?} | ConventionalRouting | GET / | / |
+| :broken_heart: | [2](#2) | /ConventionalRoute/ActionWithStringParameter/2 | ConventionalRoute/ActionWithStringParameter/{id?} | ConventionalRouting | GET /ConventionalRoute/ActionWithStringParameter/2?num=3 | /ConventionalRoute/ActionWithStringParameter/2 |
+| :broken_heart: | [3](#3) | /ConventionalRoute/ActionWithStringParameter | ConventionalRoute/ActionWithStringParameter/{id?} | ConventionalRouting | GET /ConventionalRoute/ActionWithStringParameter?num=3 | /ConventionalRoute/ActionWithStringParameter |
+| :broken_heart: | [4](#4) | /ConventionalRoute/NotFound | | ConventionalRouting | GET /ConventionalRoute/NotFound | /ConventionalRoute/NotFound |
+| :broken_heart: | [5](#5) | /SomePath/SomeString/2 | SomePath/{id}/{num:int} | ConventionalRouting | GET /SomePath/SomeString/2 | /SomePath/SomeString/2 |
+| :broken_heart: | [6](#6) | /SomePath/SomeString/NotAnInt | | ConventionalRouting | GET /SomePath/SomeString/NotAnInt | /SomePath/SomeString/NotAnInt |
+| :broken_heart: | [7](#7) | /MyArea | {area:exists}/ControllerForMyArea/Default/{id?} | ConventionalRouting | GET /MyArea | /MyArea |
+| :broken_heart: | [8](#8) | /MyArea/ControllerForMyArea/NonDefault | {area:exists}/ControllerForMyArea/NonDefault/{id?} | ConventionalRouting | GET /MyArea/ControllerForMyArea/NonDefault | /MyArea/ControllerForMyArea/NonDefault |
+| :broken_heart: | [9](#9) | /SomePrefix | SomePrefix/AnotherArea/Index/{id?} | ConventionalRouting | GET /SomePrefix | /SomePrefix |
+| :green_heart: | [10](#10) | AttributeRoute | AttributeRoute | AttributeRouting | GET /AttributeRoute | AttributeRoute |
+| :green_heart: | [11](#11) | AttributeRoute/Get | AttributeRoute/Get | AttributeRouting | GET /AttributeRoute/Get | AttributeRoute/Get |
+| :green_heart: | [12](#12) | AttributeRoute/Get/{id} | AttributeRoute/Get/{id} | AttributeRouting | GET /AttributeRoute/Get/12 | AttributeRoute/Get/{id} |
+| :green_heart: | [13](#13) | AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate | AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate | AttributeRouting | GET /AttributeRoute/12/GetWithActionNameInDifferentSpotInTemplate | AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate |
+| :green_heart: | [14](#14) | AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate | AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate | AttributeRouting | GET /AttributeRoute/NotAnInt/GetWithActionNameInDifferentSpotInTemplate | AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate |
+| :broken_heart: | [15](#15) | / | /Index | RazorPages | GET / | / |
+| :broken_heart: | [16](#16) | Index | /Index | RazorPages | GET /Index | Index |
+| :broken_heart: | [17](#17) | PageThatThrowsException | /PageThatThrowsException | RazorPages | GET /PageThatThrowsException | PageThatThrowsException |
+| :broken_heart: | [18](#18) | /js/site.js | | RazorPages | GET /js/site.js | /js/site.js |
+| :broken_heart: | [19](#19) | /MinimalApi | TBD | MinimalApi | GET /MinimalApi | /MinimalApi |
+| :broken_heart: | [20](#20) | /MinimalApi/123 | TBD | MinimalApi | GET /MinimalApi/123 | /MinimalApi/123 |
+
+#### 1
+
+```json
+{
+ "HttpMethod": "GET",
+ "Path": "/",
+ "HttpRouteByRawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "HttpRouteByControllerActionAndParameters": "ConventionalRoute/Default",
+ "HttpRouteByActionDescriptor": "ConventionalRoute/Default/{id?}",
+ "DebugInfo": {
+ "RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "RouteDiagnosticMetadata": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "RouteData": {
+ "controller": "ConventionalRoute",
+ "action": "Default"
+ },
+ "AttributeRouteInfo": null,
+ "ActionParameters": [],
+ "PageActionDescriptorRelativePath": null,
+ "PageActionDescriptorViewEnginePath": null,
+ "ControllerActionDescriptorControllerName": "ConventionalRoute",
+ "ControllerActionDescriptorActionName": "Default"
+ }
+}
+```
+
+#### 2
+
+```json
+{
+ "HttpMethod": "GET",
+ "Path": "/ConventionalRoute/ActionWithStringParameter/2?num=3",
+ "HttpRouteByRawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "HttpRouteByControllerActionAndParameters": "ConventionalRoute/ActionWithStringParameter/{id}/{num}",
+ "HttpRouteByActionDescriptor": "ConventionalRoute/ActionWithStringParameter/{id?}",
+ "DebugInfo": {
+ "RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "RouteDiagnosticMetadata": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "RouteData": {
+ "controller": "ConventionalRoute",
+ "action": "ActionWithStringParameter",
+ "id": "2"
+ },
+ "AttributeRouteInfo": null,
+ "ActionParameters": [
+ "id",
+ "num"
+ ],
+ "PageActionDescriptorRelativePath": null,
+ "PageActionDescriptorViewEnginePath": null,
+ "ControllerActionDescriptorControllerName": "ConventionalRoute",
+ "ControllerActionDescriptorActionName": "ActionWithStringParameter"
+ }
+}
+```
+
+#### 3
+
+```json
+{
+ "HttpMethod": "GET",
+ "Path": "/ConventionalRoute/ActionWithStringParameter?num=3",
+ "HttpRouteByRawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "HttpRouteByControllerActionAndParameters": "ConventionalRoute/ActionWithStringParameter/{id}/{num}",
+ "HttpRouteByActionDescriptor": "ConventionalRoute/ActionWithStringParameter/{id?}",
+ "DebugInfo": {
+ "RawText": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "RouteDiagnosticMetadata": "{controller=ConventionalRoute}/{action=Default}/{id?}",
+ "RouteData": {
+ "controller": "ConventionalRoute",
+ "action": "ActionWithStringParameter"
+ },
+ "AttributeRouteInfo": null,
+ "ActionParameters": [
+ "id",
+ "num"
+ ],
+ "PageActionDescriptorRelativePath": null,
+ "PageActionDescriptorViewEnginePath": null,
+ "ControllerActionDescriptorControllerName": "ConventionalRoute",
+ "ControllerActionDescriptorActionName": "ActionWithStringParameter"
+ }
+}
+```
+
+#### 4
+
+```json
+{
+ "HttpMethod": "GET",
+ "Path": "/ConventionalRoute/NotFound",
+ "HttpRouteByRawText": null,
+ "HttpRouteByControllerActionAndParameters": "",
+ "HttpRouteByActionDescriptor": "",
+ "DebugInfo": {
+ "RawText": null,
+ "RouteDiagnosticMetadata": null,
+ "RouteData": {},
+ "AttributeRouteInfo": null,
+ "ActionParameters": null,
+ "PageActionDescriptorRelativePath": null,
+ "PageActionDescriptorViewEnginePath": null,
+ "ControllerActionDescriptorControllerName": null,
+ "ControllerActionDescriptorActionName": null
+ }
+}
+```
+
+#### 5
+
+```json
+{
+ "HttpMethod": "GET",
+ "Path": "/SomePath/SomeString/2",
+ "HttpRouteByRawText": "SomePath/{id}/{num:int}",
+ "HttpRouteByControllerActionAndParameters": "ConventionalRoute/ActionWithStringParameter/{id}/{num}",
+ "HttpRouteByActionDescriptor": "SomePath/{id}/{num:int}",
+ "DebugInfo": {
+ "RawText": "SomePath/{id}/{num:int}",
+ "RouteDiagnosticMetadata": "SomePath/{id}/{num:int}",
+ "RouteData": {
+ "controller": "ConventionalRoute",
+ "action": "ActionWithStringParameter",
+ "id": "SomeString",
+ "num": "2"
+ },
+ "AttributeRouteInfo": null,
+ "ActionParameters": [
+ "id",
+ "num"
+ ],
+ "PageActionDescriptorRelativePath": null,
+ "PageActionDescriptorViewEnginePath": null,
+ "ControllerActionDescriptorControllerName": "ConventionalRoute",
+ "ControllerActionDescriptorActionName": "ActionWithStringParameter"
+ }
+}
+```
+
+#### 6
+
+```json
+{
+ "HttpMethod": "GET",
+ "Path": "/SomePath/SomeString/NotAnInt",
+ "HttpRouteByRawText": null,
+ "HttpRouteByControllerActionAndParameters": "",
+ "HttpRouteByActionDescriptor": "",
+ "DebugInfo": {
+ "RawText": null,
+ "RouteDiagnosticMetadata": null,
+ "RouteData": {},
+ "AttributeRouteInfo": null,
+ "ActionParameters": null,
+ "PageActionDescriptorRelativePath": null,
+ "PageActionDescriptorViewEnginePath": null,
+ "ControllerActionDescriptorControllerName": null,
+ "ControllerActionDescriptorActionName": null
+ }
+}
+```
+
+#### 7
+
+```json
+{
+ "HttpMethod": "GET",
+ "Path": "/MyArea",
+ "HttpRouteByRawText": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
+ "HttpRouteByControllerActionAndParameters": "ControllerForMyArea/Default",
+ "HttpRouteByActionDescriptor": "{area:exists}/ControllerForMyArea/Default/{id?}",
+ "DebugInfo": {
+ "RawText": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
+ "RouteDiagnosticMetadata": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
+ "RouteData": {
+ "controller": "ControllerForMyArea",
+ "action": "Default",
+ "area": "MyArea"
+ },
+ "AttributeRouteInfo": null,
+ "ActionParameters": [],
+ "PageActionDescriptorRelativePath": null,
+ "PageActionDescriptorViewEnginePath": null,
+ "ControllerActionDescriptorControllerName": "ControllerForMyArea",
+ "ControllerActionDescriptorActionName": "Default"
+ }
+}
+```
+
+#### 8
+
+```json
+{
+ "HttpMethod": "GET",
+ "Path": "/MyArea/ControllerForMyArea/NonDefault",
+ "HttpRouteByRawText": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
+ "HttpRouteByControllerActionAndParameters": "ControllerForMyArea/NonDefault",
+ "HttpRouteByActionDescriptor": "{area:exists}/ControllerForMyArea/NonDefault/{id?}",
+ "DebugInfo": {
+ "RawText": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
+ "RouteDiagnosticMetadata": "{area:exists}/{controller=ControllerForMyArea}/{action=Default}/{id?}",
+ "RouteData": {
+ "controller": "ControllerForMyArea",
+ "area": "MyArea",
+ "action": "NonDefault"
+ },
+ "AttributeRouteInfo": null,
+ "ActionParameters": [],
+ "PageActionDescriptorRelativePath": null,
+ "PageActionDescriptorViewEnginePath": null,
+ "ControllerActionDescriptorControllerName": "ControllerForMyArea",
+ "ControllerActionDescriptorActionName": "NonDefault"
+ }
+}
+```
+
+#### 9
+
+```json
+{
+ "HttpMethod": "GET",
+ "Path": "/SomePrefix",
+ "HttpRouteByRawText": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}",
+ "HttpRouteByControllerActionAndParameters": "AnotherArea/Index",
+ "HttpRouteByActionDescriptor": "SomePrefix/AnotherArea/Index/{id?}",
+ "DebugInfo": {
+ "RawText": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}",
+ "RouteDiagnosticMetadata": "SomePrefix/{controller=AnotherArea}/{action=Index}/{id?}",
+ "RouteData": {
+ "area": "AnotherArea",
+ "controller": "AnotherArea",
+ "action": "Index"
+ },
+ "AttributeRouteInfo": null,
+ "ActionParameters": [],
+ "PageActionDescriptorRelativePath": null,
+ "PageActionDescriptorViewEnginePath": null,
+ "ControllerActionDescriptorControllerName": "AnotherArea",
+ "ControllerActionDescriptorActionName": "Index"
+ }
+}
+```
+
+#### 10
+
+```json
+{
+ "HttpMethod": "GET",
+ "Path": "/AttributeRoute",
+ "HttpRouteByRawText": "AttributeRoute",
+ "HttpRouteByControllerActionAndParameters": "AttributeRoute/Get",
+ "HttpRouteByActionDescriptor": "AttributeRoute",
+ "DebugInfo": {
+ "RawText": "AttributeRoute",
+ "RouteDiagnosticMetadata": "AttributeRoute",
+ "RouteData": {
+ "action": "Get",
+ "controller": "AttributeRoute"
+ },
+ "AttributeRouteInfo": "AttributeRoute",
+ "ActionParameters": [],
+ "PageActionDescriptorRelativePath": null,
+ "PageActionDescriptorViewEnginePath": null,
+ "ControllerActionDescriptorControllerName": "AttributeRoute",
+ "ControllerActionDescriptorActionName": "Get"
+ }
+}
+```
+
+#### 11
+
+```json
+{
+ "HttpMethod": "GET",
+ "Path": "/AttributeRoute/Get",
+ "HttpRouteByRawText": "AttributeRoute/Get",
+ "HttpRouteByControllerActionAndParameters": "AttributeRoute/Get",
+ "HttpRouteByActionDescriptor": "AttributeRoute/Get",
+ "DebugInfo": {
+ "RawText": "AttributeRoute/Get",
+ "RouteDiagnosticMetadata": "AttributeRoute/Get",
+ "RouteData": {
+ "action": "Get",
+ "controller": "AttributeRoute"
+ },
+ "AttributeRouteInfo": "AttributeRoute/Get",
+ "ActionParameters": [],
+ "PageActionDescriptorRelativePath": null,
+ "PageActionDescriptorViewEnginePath": null,
+ "ControllerActionDescriptorControllerName": "AttributeRoute",
+ "ControllerActionDescriptorActionName": "Get"
+ }
+}
+```
+
+#### 12
+
+```json
+{
+ "HttpMethod": "GET",
+ "Path": "/AttributeRoute/Get/12",
+ "HttpRouteByRawText": "AttributeRoute/Get/{id}",
+ "HttpRouteByControllerActionAndParameters": "AttributeRoute/Get/{id}",
+ "HttpRouteByActionDescriptor": "AttributeRoute/Get/{id}",
+ "DebugInfo": {
+ "RawText": "AttributeRoute/Get/{id}",
+ "RouteDiagnosticMetadata": "AttributeRoute/Get/{id}",
+ "RouteData": {
+ "action": "Get",
+ "controller": "AttributeRoute",
+ "id": "12"
+ },
+ "AttributeRouteInfo": "AttributeRoute/Get/{id}",
+ "ActionParameters": [
+ "id"
+ ],
+ "PageActionDescriptorRelativePath": null,
+ "PageActionDescriptorViewEnginePath": null,
+ "ControllerActionDescriptorControllerName": "AttributeRoute",
+ "ControllerActionDescriptorActionName": "Get"
+ }
+}
+```
+
+#### 13
+
+```json
+{
+ "HttpMethod": "GET",
+ "Path": "/AttributeRoute/12/GetWithActionNameInDifferentSpotInTemplate",
+ "HttpRouteByRawText": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "HttpRouteByControllerActionAndParameters": "AttributeRoute/GetWithActionNameInDifferentSpotInTemplate/{id}",
+ "HttpRouteByActionDescriptor": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "DebugInfo": {
+ "RawText": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "RouteDiagnosticMetadata": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "RouteData": {
+ "action": "GetWithActionNameInDifferentSpotInTemplate",
+ "controller": "AttributeRoute",
+ "id": "12"
+ },
+ "AttributeRouteInfo": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "ActionParameters": [
+ "id"
+ ],
+ "PageActionDescriptorRelativePath": null,
+ "PageActionDescriptorViewEnginePath": null,
+ "ControllerActionDescriptorControllerName": "AttributeRoute",
+ "ControllerActionDescriptorActionName": "GetWithActionNameInDifferentSpotInTemplate"
+ }
+}
+```
+
+#### 14
+
+```json
+{
+ "HttpMethod": "GET",
+ "Path": "/AttributeRoute/NotAnInt/GetWithActionNameInDifferentSpotInTemplate",
+ "HttpRouteByRawText": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "HttpRouteByControllerActionAndParameters": "AttributeRoute/GetWithActionNameInDifferentSpotInTemplate/{id}",
+ "HttpRouteByActionDescriptor": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "DebugInfo": {
+ "RawText": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "RouteDiagnosticMetadata": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "RouteData": {
+ "action": "GetWithActionNameInDifferentSpotInTemplate",
+ "controller": "AttributeRoute",
+ "id": "NotAnInt"
+ },
+ "AttributeRouteInfo": "AttributeRoute/{id}/GetWithActionNameInDifferentSpotInTemplate",
+ "ActionParameters": [
+ "id"
+ ],
+ "PageActionDescriptorRelativePath": null,
+ "PageActionDescriptorViewEnginePath": null,
+ "ControllerActionDescriptorControllerName": "AttributeRoute",
+ "ControllerActionDescriptorActionName": "GetWithActionNameInDifferentSpotInTemplate"
+ }
+}
+```
+
+#### 15
+
+```json
+{
+ "HttpMethod": "GET",
+ "Path": "/",
+ "HttpRouteByRawText": "",
+ "HttpRouteByControllerActionAndParameters": "",
+ "HttpRouteByActionDescriptor": "/Index",
+ "DebugInfo": {
+ "RawText": "",
+ "RouteDiagnosticMetadata": "",
+ "RouteData": {
+ "page": "/Index"
+ },
+ "AttributeRouteInfo": "",
+ "ActionParameters": [],
+ "PageActionDescriptorRelativePath": "/Pages/Index.cshtml",
+ "PageActionDescriptorViewEnginePath": "/Index",
+ "ControllerActionDescriptorControllerName": null,
+ "ControllerActionDescriptorActionName": null
+ }
+}
+```
+
+#### 16
+
+```json
+{
+ "HttpMethod": "GET",
+ "Path": "/Index",
+ "HttpRouteByRawText": "Index",
+ "HttpRouteByControllerActionAndParameters": "",
+ "HttpRouteByActionDescriptor": "/Index",
+ "DebugInfo": {
+ "RawText": "Index",
+ "RouteDiagnosticMetadata": "Index",
+ "RouteData": {
+ "page": "/Index"
+ },
+ "AttributeRouteInfo": "Index",
+ "ActionParameters": [],
+ "PageActionDescriptorRelativePath": "/Pages/Index.cshtml",
+ "PageActionDescriptorViewEnginePath": "/Index",
+ "ControllerActionDescriptorControllerName": null,
+ "ControllerActionDescriptorActionName": null
+ }
+}
+```
+
+#### 17
+
+```json
+{
+ "HttpMethod": "GET",
+ "Path": "/PageThatThrowsException",
+ "HttpRouteByRawText": "PageThatThrowsException",
+ "HttpRouteByControllerActionAndParameters": "",
+ "HttpRouteByActionDescriptor": "/PageThatThrowsException",
+ "DebugInfo": {
+ "RawText": "PageThatThrowsException",
+ "RouteDiagnosticMetadata": "PageThatThrowsException",
+ "RouteData": {
+ "page": "/PageThatThrowsException"
+ },
+ "AttributeRouteInfo": "PageThatThrowsException",
+ "ActionParameters": [],
+ "PageActionDescriptorRelativePath": "/Pages/PageThatThrowsException.cshtml",
+ "PageActionDescriptorViewEnginePath": "/PageThatThrowsException",
+ "ControllerActionDescriptorControllerName": null,
+ "ControllerActionDescriptorActionName": null
+ }
+}
+```
+
+#### 18
+
+```json
+{
+ "HttpMethod": "GET",
+ "Path": "/js/site.js",
+ "HttpRouteByRawText": null,
+ "HttpRouteByControllerActionAndParameters": "",
+ "HttpRouteByActionDescriptor": "",
+ "DebugInfo": {
+ "RawText": null,
+ "RouteDiagnosticMetadata": null,
+ "RouteData": {},
+ "AttributeRouteInfo": null,
+ "ActionParameters": null,
+ "PageActionDescriptorRelativePath": null,
+ "PageActionDescriptorViewEnginePath": null,
+ "ControllerActionDescriptorControllerName": null,
+ "ControllerActionDescriptorActionName": null
+ }
+}
+```
+
+#### 19
+
+```json
+{
+ "HttpMethod": "GET",
+ "Path": "/MinimalApi",
+ "HttpRouteByRawText": null,
+ "HttpRouteByControllerActionAndParameters": "",
+ "HttpRouteByActionDescriptor": "",
+ "DebugInfo": {
+ "RawText": null,
+ "RouteDiagnosticMetadata": null,
+ "RouteData": {},
+ "AttributeRouteInfo": null,
+ "ActionParameters": null,
+ "PageActionDescriptorRelativePath": null,
+ "PageActionDescriptorViewEnginePath": null,
+ "ControllerActionDescriptorControllerName": null,
+ "ControllerActionDescriptorActionName": null
+ }
+}
+```
+
+#### 20
+
+```json
+{
+ "HttpMethod": "GET",
+ "Path": "/MinimalApi/123",
+ "HttpRouteByRawText": null,
+ "HttpRouteByControllerActionAndParameters": "",
+ "HttpRouteByActionDescriptor": "",
+ "DebugInfo": {
+ "RawText": null,
+ "RouteDiagnosticMetadata": null,
+ "RouteData": {},
+ "AttributeRouteInfo": null,
+ "ActionParameters": null,
+ "PageActionDescriptorRelativePath": null,
+ "PageActionDescriptorViewEnginePath": null,
+ "ControllerActionDescriptorControllerName": null,
+ "ControllerActionDescriptorActionName": null
+ }
+}
+```
diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/ReadmeGenerator.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/ReadmeGenerator.cs
new file mode 100644
index 00000000000..beefcaca3d6
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/ReadmeGenerator.cs
@@ -0,0 +1,78 @@
+//
+// 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.
+//
+
+#nullable disable
+
+using System.Text;
+using RouteTests;
+
+public class ReadmeGenerator
+{
+ public static void Main()
+ {
+ var sb = new StringBuilder();
+
+ var testCases = RoutingTests.TestData;
+
+ var results = new List();
+
+ foreach (var item in testCases)
+ {
+ using var tests = new RoutingTests();
+ var testCase = item[0] as RouteTestData.RouteTestCase;
+ var result = tests.TestRoutes(testCase!).GetAwaiter().GetResult();
+ results.Add(result);
+ }
+
+ sb.AppendLine("| | | display name | expected name (w/o http.method) | routing type | request |");
+ sb.AppendLine("| - | - | - | - | - | - |");
+
+ for (var i = 0; i < results.Count; ++i)
+ {
+ var result = results[i];
+ var emoji = result.ActivityDisplayName.Equals(result.TestCase.ExpectedHttpRoute, StringComparison.InvariantCulture)
+ ? ":green_heart:"
+ : ":broken_heart:";
+ sb.Append($"| {emoji} | [{i + 1}](#{i + 1}) ");
+ sb.AppendLine(FormatTestResult(results[i]));
+ }
+
+ for (var i = 0; i < results.Count; ++i)
+ {
+ sb.AppendLine();
+ sb.AppendLine($"#### {i + 1}");
+ sb.AppendLine();
+ sb.AppendLine("```json");
+ sb.AppendLine(results[i].RouteInfo.ToString());
+ sb.AppendLine("```");
+ }
+
+ File.WriteAllText("README.md", sb.ToString());
+
+ string FormatTestResult(TestResult result)
+ {
+ var testCase = result.TestCase!;
+
+ return $"| {string.Join(
+ " | ",
+ result.ActivityDisplayName, // TODO: should be result.HttpRoute, but http.route is not currently added to Activity
+ testCase.ExpectedHttpRoute,
+ testCase.TestApplicationScenario,
+ $"{testCase.HttpMethod} {testCase.Path}",
+ result.ActivityDisplayName)} |";
+ }
+ }
+}
diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/ReadmeGenerator.csproj b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/ReadmeGenerator.csproj
new file mode 100644
index 00000000000..2546cd772db
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/ReadmeGenerator.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net8.0
+ ReadmeGenerator
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+ RouteTests.testcases.json
+ Always
+
+
+
+
diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RouteInfo.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RouteInfo.cs
new file mode 100644
index 00000000000..593358a9779
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RouteInfo.cs
@@ -0,0 +1,137 @@
+//
+// 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.
+//
+
+#nullable enable
+
+using System.Text.Json;
+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;
+
+public class RouteInfo
+{
+ private static readonly JsonSerializerOptions JsonSerializerOptions = new() { WriteIndented = true };
+
+ public RouteInfo()
+ {
+ this.DebugInfo = new DebugInfo();
+ }
+
+ public string? HttpMethod { get; set; }
+
+ public string? Path { get; set; }
+
+ public string? HttpRouteByRawText => this.DebugInfo.RawText;
+
+ public string HttpRouteByControllerActionAndParameters
+ {
+ get
+ {
+ var condition = !string.IsNullOrEmpty(this.DebugInfo.ControllerActionDescriptorActionName)
+ && !string.IsNullOrEmpty(this.DebugInfo.ControllerActionDescriptorActionName)
+ && this.DebugInfo.ActionParameters != null;
+
+ if (!condition)
+ {
+ return string.Empty;
+ }
+
+ var paramList = string.Join(string.Empty, this.DebugInfo.ActionParameters!.Select(p => $"/{{{p}}}"));
+ return $"{this.DebugInfo.ControllerActionDescriptorControllerName}/{this.DebugInfo.ControllerActionDescriptorActionName}{paramList}";
+ }
+ }
+
+ public string HttpRouteByActionDescriptor
+ {
+ get
+ {
+ var result = string.Empty;
+
+ var hasControllerActionDescriptor = this.DebugInfo.ControllerActionDescriptorControllerName != null
+ && this.DebugInfo.ControllerActionDescriptorActionName != null;
+
+ var hasPageActionDescriptor = this.DebugInfo.PageActionDescriptorRelativePath != null
+ && this.DebugInfo.PageActionDescriptorViewEnginePath != null;
+
+ if (this.DebugInfo.RawText != null && hasControllerActionDescriptor)
+ {
+ var controllerRegex = new System.Text.RegularExpressions.Regex(@"\{controller=.*?\}+?");
+ var actionRegex = new System.Text.RegularExpressions.Regex(@"\{action=.*?\}+?");
+ result = controllerRegex.Replace(this.DebugInfo.RawText, this.DebugInfo.ControllerActionDescriptorControllerName!);
+ result = actionRegex.Replace(result, this.DebugInfo.ControllerActionDescriptorActionName!);
+ }
+ else if (this.DebugInfo.RawText != null && hasPageActionDescriptor)
+ {
+ result = this.DebugInfo.PageActionDescriptorViewEnginePath!;
+ }
+
+ return result;
+ }
+ }
+
+ public DebugInfo DebugInfo { 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.DebugInfo.RawText = (endpoint as RouteEndpoint)?.RoutePattern.RawText;
+#if NET8_0_OR_GREATER
+ this.DebugInfo.RouteDiagnosticMetadata = endpoint?.Metadata.GetMetadata()?.Route;
+#endif
+ this.DebugInfo.RouteData = new Dictionary();
+ foreach (var value in context.GetRouteData().Values)
+ {
+ this.DebugInfo.RouteData[value.Key] = value.Value?.ToString();
+ }
+ }
+
+ public void SetValues(ActionDescriptor actionDescriptor)
+ {
+ this.DebugInfo.AttributeRouteInfo = actionDescriptor.AttributeRouteInfo?.Template;
+
+ this.DebugInfo.ActionParameters = new List();
+ foreach (var item in actionDescriptor.Parameters)
+ {
+ this.DebugInfo.ActionParameters.Add(item.Name);
+ }
+
+ if (actionDescriptor is PageActionDescriptor pad)
+ {
+ this.DebugInfo.PageActionDescriptorRelativePath = pad.RelativePath;
+ this.DebugInfo.PageActionDescriptorViewEnginePath = pad.ViewEnginePath;
+ }
+
+ if (actionDescriptor is ControllerActionDescriptor cad)
+ {
+ this.DebugInfo.ControllerActionDescriptorControllerName = cad.ControllerName;
+ this.DebugInfo.ControllerActionDescriptorActionName = cad.ActionName;
+ }
+ }
+
+ public override string ToString()
+ {
+ return JsonSerializer.Serialize(this, JsonSerializerOptions);
+ }
+}
diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RouteInfoMiddleware.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RouteInfoMiddleware.cs
new file mode 100644
index 00000000000..0623884e74d
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RouteInfoMiddleware.cs
@@ -0,0 +1,83 @@
+//
+// 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.
+//
+
+#nullable disable
+
+using System.Diagnostics;
+using System.Text.Json;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using static System.Net.Mime.MediaTypeNames;
+
+namespace RouteTests;
+
+public class RouteInfoMiddleware
+{
+ private readonly RequestDelegate next;
+
+ public RouteInfoMiddleware(RequestDelegate next)
+ {
+ this.next = next;
+ }
+
+ public static void ConfigureExceptionHandler(IApplicationBuilder builder)
+ {
+ builder.Run(async context =>
+ {
+ context.Response.Body = (context.Items["originBody"] as Stream)!;
+
+ context.Response.ContentType = Application.Json;
+
+ var info = context.Items["RouteInfo"] as RouteInfo;
+ Debug.Assert(info != null, "RouteInfo object not present in context.Items");
+ var jsonOptions = new JsonSerializerOptions { WriteIndented = true };
+ string modifiedResponse = JsonSerializer.Serialize(info, jsonOptions);
+ await context.Response.WriteAsync(modifiedResponse);
+ });
+ }
+
+ public async Task InvokeAsync(HttpContext context)
+ {
+ var response = context.Response;
+
+ var originBody = response.Body;
+ context.Items["originBody"] = originBody;
+ using var newBody = new MemoryStream();
+ response.Body = newBody;
+
+ await this.next(context);
+
+ var stream = response.Body;
+ using var reader = new StreamReader(stream, leaveOpen: true);
+ var originalResponse = await reader.ReadToEndAsync();
+
+ var info = context.Items["RouteInfo"] as RouteInfo;
+ Debug.Assert(info != null, "RouteInfo object not present in context.Items");
+ var jsonOptions = new JsonSerializerOptions { WriteIndented = true };
+ string modifiedResponse = JsonSerializer.Serialize(info, jsonOptions);
+
+ stream.SetLength(0);
+ using var writer = new StreamWriter(stream, leaveOpen: true);
+ await writer.WriteAsync(modifiedResponse);
+ await writer.FlushAsync();
+ response.ContentLength = stream.Length;
+ response.ContentType = "application/json";
+
+ newBody.Seek(0, SeekOrigin.Begin);
+ await newBody.CopyToAsync(originBody);
+ response.Body = originBody;
+ }
+}
diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RouteTestData.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RouteTestData.cs
new file mode 100644
index 00000000000..f4f5b5c65d0
--- /dev/null
+++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/RouteTests/RouteTestData.cs
@@ -0,0 +1,82 @@
+//
+// 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.
+//
+
+#nullable enable
+
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace RouteTests;
+
+public static class RouteTestData
+{
+ public static IEnumerable