Capturing ASP.NET Core original URL with Azure Application Insights
Tags: azure, dotnet, aspnet, csharp
Published: 2020-09-14 15:40:20

---
title: Capturing ASP.NET Core original URL with Azure Application Insights
published: true
date: 2020-09-14 00:00:00 UTC
tags: azure, dotnet, aspnet, csharp
canonical_url: https://swimburger.net/blog/azure/capturing-aspnet-core-original-url-with-azure-application-insights
---

Azure Application Insights is an Application Performance Management (APM) tool providing insights into the state of your application. By default, Application Insights will capture a lot of data about your ASP.NET Core applications including HTTP Requests made to your website. Unfortunately, the **URL captured by Application Insights doesn't always match the URL originally requested by the client**.

In many ASP.NET applications, **the path of the HTTP request is internally rewritten by the time Application Insights records the data**. This results in the **rewritten URL to be captured and not the original URL**. You can simulate this scenario by using the URL Rewriting Middleware provided by using the `Microsoft.AspNetCore.Rewrite` package which is already included implicitly in ASP.NET Core apps. 

In the `Startup` class, update the `Configure` method by adding the following `RewriteOptions` and passing it to `app.UseRewriter` to configure the rewrite middleware.

```csharp
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace AddRawUrlToApplicationInsights_Core
{
    public class Startup
    {
        ...
        
        public void Configure(IApplicationBuilder app)
        {
            // this is for demonstration purposes only
            // it's recommended to use a redirect (not rewrite) in these scenario's for SEO purposes to avoid duplicate URL's
            var options = new RewriteOptions()
                .AddRewrite(@"^privacy$", "home/privacy", false);

            app.UseRewriter(options);

            ...
        }
    }
}
```

[Source at GitHub](https://github.com/Swimburger/AddRawUrlToApplicationInsights-Core/blob/master/Startup.cs)

In this sample, when `/privacy` is requested, the path is rewritten to `/home/privacy` which will resolve to the `HomeController.Privacy` action. When you browse to `/privacy` and look at the recorded HTTP request inside of Application Insights, the URL will be `https://domain.tld/home/privacy` instead of `https://domain.tld/privacy`. The rewritten URL is useful information, but you may also need the original URL for proper debugging/analyzing.

Luckily, you can access the original path before it was rewritten by calling `httpContext.Features.Get<IHttpRequestFeature>()` and then grabbing the `RawTarget` property.

> " **IHttpRequestFeature.RawTarget Property** : The request target as it was sent in the HTTP request. This property contains the raw path and full query, as well as other request targets such as \* for OPTIONS requests"  
> Source: [Microsoft Documentation](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttprequestfeature.rawtarget?view=aspnetcore-3.1#Microsoft_AspNetCore_Http_Features_IHttpRequestFeature_RawTarget)

Application Insights provides an extensibility point for you to capture additional data using the `ITelemetryInitializer` interface. You can capture the original URL by implementing `ITelemetryInitializer` and adding the `RawTarget` data to the telemetry properties:

```csharp
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using System;

namespace AddRawUrlToApplicationInsights_Core
{
    public class OriginalUrlTelemetryInitializer : ITelemetryInitializer
    {
        readonly IHttpContextAccessor httpContextAccessor;

        public OriginalUrlTelemetryInitializer(IHttpContextAccessor httpContextAccessor)
        {
            this.httpContextAccessor = httpContextAccessor;
        }

        public void Initialize(ITelemetry telemetry)
        {
            if (telemetry is RequestTelemetry requestTelemetry)
            {
                var httpContext = httpContextAccessor.HttpContext;
                if (httpContext?.Request == null)
                {
                    return;
                }

                var request = httpContext.Request;
                var httpRequestFeature = httpContext.Features.Get<IHttpRequestFeature>();
                var rawTarget = httpRequestFeature.RawTarget; // RawTarget returns original path and querystring
                requestTelemetry.Properties["OriginalUrl"] = rawTarget;

                // to extract the path and query from the RawTarget, you need to use the Uri class
                // the Uri class requires at least the scheme://host to parse;
                // adding the dummy base uri "http://0.0.0.0" will ensure it parses properly
                var rawUri = new Uri($"http://0.0.0.0{rawTarget}"); 
                var fdnUrl = new UriBuilder(
                    scheme: request.Scheme,
                    host: request.Host.Host,
                    port: request.Host.Port ?? -1, // use -1 in case there's no port specified, the port will be left out when converting `ToString`
                    path: rawUri.AbsolutePath,
                    extraValue: rawUri.Query
                ).ToString();
                requestTelemetry.Properties["OriginalUrlFqdn"] = fdnUrl;
            }
        }
    }
}
```

[Source at GitHub](https://github.com/Swimburger/AddRawUrlToApplicationInsights-Core/blob/master/OriginalUrlTelemetryInitializer.cs)

Additionally, you can grab the `HttpRequest` by grabbing the `HttpContext` via the `IHttpContextAccessor`. The `IHttpContextAccessor` is automatically injected by ASP.NET's dependency injection.  
Using the Uri class, you can extract the path and querystring out of the `RawTarget` and combine it with the `HttpRequest` data to create the original URL including the fully qualified domain name.

Next, you need to add the `OriginalUrlTelemetryInitializer` to ASP.NET Core's dependency injection system. Update the `Startup` class as below:

```csharp
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace AddRawUrlToApplicationInsights_Core
{
    public class Startup
    {
        ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
            services.AddApplicationInsightsTelemetry();
            // register OriginalUrlTelemetryInitializer to make it available to Application Insights 
            services.AddSingleton<ITelemetryInitializer, OriginalUrlTelemetryInitializer>();
        }

        ...
    }
}
```

[Full source at GitHub](https://github.com/Swimburger/AddRawUrlToApplicationInsights-Core/blob/master/Startup.cs)

This is what the resulting data looks like when sent to Application Insights:

```json
{
  "name": "AppRequests",
  "time": "2020-09-12T04:12:47.768786Z",
  "tags": {
    "ai.application.ver": "1.0.0.0",
    "ai.cloud.roleInstance": "yourhostname",
    "ai.operation.id": "47519341849a5d448f363d488ef3a71e",
    "ai.operation.name": "GET Home/Privacy",
    "ai.location.ip": "::1",
    "ai.internal.sdkVersion": "aspnet5c:2.14.0",
    "ai.internal.nodeName": "yourhostname"
  },
  "data": {
    "baseType": "RequestData",
    "baseData": {
      "ver": 2,
      "id": "925c17c66525b141",
      "name": "GET Home/Privacy",
      "duration": "00:00:00.0179295",
      "success": true,
      "responseCode": "200",
      "url": "https://localhost:5001/home/privacy",
      "properties": {
        "OriginalUrl": "/privacy",
        "OriginalUrlFqdn": "https://localhost:5001/privacy",
        "DeveloperMode": "true",
        "AspNetCoreEnvironment": "Development",
        "_MS.ProcessedByMetricExtractors": "(Name:'Requests', Ver:'1.1')"
      }
    }
  }
}
```

Notice how the `url`, `properties.OriginalUrl`, and `properties.OriginalUrlFqdn` are all captured, providing a more complete picture of the request:

- url: https://localhost:5001/home/privacy
- properties.OriginalUrl: /privacy
- properties.OriginalUrlFqdn: https://localhost:5001/privacy

## Summary

Using the `ITelemetryInitializer` extensibility point in Application Insights, you can capture additional data. Using the `IHttpRequestFeature.RawTarget` property, you can capture the original URL in addition to the rewritten URL which is captured by default.