Tracking API calls by User in .NET Core APIs

When it comes to leveraging API analytics and metrics, simply tracking API calls by anonymous usage is not enough to gain valuable insights. It’s essential to understand how specific endpoints are being used by different users to get a more complete picture of API usage. This information can help optimize the performance of the API and improve the user experience.

To track API calls and attribute them to a specific user, authentication is required. Authentication is the process of verifying the identity of the user making the API call. In this example, a JSON Web Token (JWT) can be used to authenticate the API calls.

I will be working from the .NET Core code example from a previous guide, available here.

To get user tracking working we will:

  • Attach a JWT to our request, and send it to the API
  • Show how the IdentifyUser function we created in the previous guide works with an attached JWT Bearer token

A few things to mention in this guide, before we start.

  1. I will be generating a JWT from jwt.io. Normally you would use the authentication flow from your app in order to generate this upon login, or by other means that do not require manual creation. In order to make things easier though, I have opted for manual creation. You will want to ensure that your JWT does have the required fields though in order to make user identification work.

  2. This example API is not secured. It uses a JWT is passed with the request but we are not doing any type of authorization checks in this example. When publishing an API to production, likely you will want to make sure you have the correct authorization flows in place.

  3. Our example assumes that your server and API are already integrated with Moesif. If you still need to integrate your API with Moesif, please take a look at our .NET Core integration guide before proceeding if you want to follow along.

With that said, let’s jump into our example!

Step 1: Getting Started

Below is the code for our sample API. It is just a very simple, single endpoint, built with .NET Core Minimal. As mentioned earlier, this server has already been integrated with Moesif.

The code for our Program.cs currently looks like this:

// Program.cs

using Moesif.Middleware;

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(serverOptions =>
{
   serverOptions.AllowSynchronousIO = true;
});

var app = builder.Build();

var moesifOptions = new CreditScoreAPI.Settings.MoesifOptions(app.Configuration);

app.UseMiddleware<MoesifMiddleware>(moesifOptions.getMoesifOptions());

var userAddedCreditScores = new List<CreditScore>();

app.MapGet("/creditscore", () =>
{
    var score = new CreditScore
    (
        Random.Shared.Next(300, 850)
    );

    return score;
});

app.MapPost("/creditscore", (CreditScore score) => {
    userAddedCreditScores.Add(score);
    return score;
});

app.MapGet("/userAddedCreditScores", () => userAddedCreditScores);

app.Run();

record CreditScore(int Score)
{
    public string? CreditRating
    {
        get => Score switch
        {
            >= 800 => "Excellent",
            >= 700 => "Good",
            >= 600 => "Fair",
            >= 500 => "Poor",
            _ => "Bad"
        };
    }
}

Our appsettings.json file houses our configuration details. It’s here we can define various options like our application ID, whether or not to log the body of a request, or enable local debugging.

{
  "MoesifOptions": {
    "ApplicationId": "YOUR_MOESIF_APPLICATION_ID",
    "LocalDebug": false,
    "LogBody": true,
    "LogBodyOutgoing": true,
    "ApiVersion": "1.1.0",
    "EnableBatching": true,
    "BatchSize": 25
  },
  ...
}

And finally our MoesifOptions.cs configuration looks like this:

// MoesifOptions.cs

namespace CreditScoreAPI.Settings
{

    public class MoesifOptions
    {
        private readonly IConfiguration _config;

        public MoesifOptions(IConfiguration config)
        {
            _config = config;
        }
        public static Func<HttpRequest, HttpResponse, string> IdentifyUser = (HttpRequest req, HttpResponse res) => {
            // Implement your custom logic to return user id
            return req.HttpContext?.User?.Identity?.Name;
        };

        public static Func<HttpRequest, HttpResponse, string> IdentifyCompany = (HttpRequest req, HttpResponse res) => {
            return req.Headers["X-Organization-Id"];
        };

        public static Func<HttpRequest, HttpResponse, string> GetSessionToken = (HttpRequest req, HttpResponse res) => {
            return req.Headers["Authorization"];
        };

        public static Func<HttpRequest, HttpResponse, Dictionary<string, object>> GetMetadata = (HttpRequest req, HttpResponse res) => {
            Dictionary<string, object> metadata = new Dictionary<string, object>
            {
                {"string_field", "value_1"},
                {"number_field", 0},
                {"object_field", new Dictionary<string, string> {
                    {"field_a", "value_a"},
                    {"field_b", "value_b"}
                    }
                }
            };
            return metadata;
        };

        public static Func<HttpRequestMessage, HttpResponseMessage, Dictionary<string, object>> GetMetadataOutgoing = (HttpRequestMessage req, HttpResponseMessage res) => {
            Dictionary<string, object> metadata = new Dictionary<string, object>
            {
                {"string_field", "value_1"},
                {"number_field", 0},
                {"object_field", new Dictionary<string, string> {
                    {"field_a", "value_a"},
                    {"field_b", "value_b"}
                    }
                }
            };
            return metadata;
        };

        public Dictionary<string, object> getMoesifOptions()
        {
            Dictionary<string, object> moesifOptions = new Dictionary<string, object>
            {
                {MoesifOptionsParamNames.ApplicationId, getConfigString(MoesifOptionsParamNames.ApplicationId)},
                {MoesifOptionsParamNames.LocalDebug, getConfigBool(MoesifOptionsParamNames.LocalDebug)},
                {MoesifOptionsParamNames.LogBody, getConfigBool(MoesifOptionsParamNames.LogBody)},
                {MoesifOptionsParamNames.LogBodyOutgoing, getConfigBool(MoesifOptionsParamNames.LogBodyOutgoing)},
                {MoesifOptionsParamNames.ApiVersion, getConfigString(MoesifOptionsParamNames.ApiVersion)},
                {MoesifOptionsParamNames.EnableBatching, getConfigBool(MoesifOptionsParamNames.EnableBatching)},
                {MoesifOptionsParamNames.BatchSize, getConfigInt(MoesifOptionsParamNames.BatchSize)},
                {MoesifOptionsParamNames.IdentifyUser, IdentifyUser},
                {MoesifOptionsParamNames.IdentifyCompany, IdentifyCompany},
                {MoesifOptionsParamNames.GetSessionToken, GetSessionToken},
                {MoesifOptionsParamNames.GetMetadata, GetMetadata},
                {MoesifOptionsParamNames.GetMetadataOutgoing, GetMetadataOutgoing}
            };
            return moesifOptions;
        }

        public string getConfigString(string paramName)
        {
            return _config.GetValue<string>(MoesifOptionsParamNames.asKey(paramName));
        }

        public bool getConfigBool(string paramName)
        {
            return _config.GetValue<bool>(MoesifOptionsParamNames.asKey(paramName));
        }
        public int getConfigInt(string paramName)
        {
            return _config.GetValue<int>(MoesifOptionsParamNames.asKey(paramName));
        }

        public bool isConfiguredMoesifApplicationId()
        {
            string appId = null;
            try
            {
                appId = (string)getMoesifOptions().GetValueOrDefault(MoesifOptionsParamNames.ApplicationId);
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error Reading Moesif Application Id in appsettings(.env).json: " + ex.Message);
            }
            return !string.IsNullOrWhiteSpace(appId) && !appId.StartsWith("<");
        }
    }

    public class MoesifOptionsParamNames
    {
        // Read from appsettings.json
        public static string Key = "MoesifOptions";
        public static string ApplicationId = "ApplicationId";
        public static string LocalDebug = "LocalDebug";
        public static string LogBody = "LogBody";
        public static string LogBodyOutgoing = "LogBodyOutgoing";
        public static string ApiVersion = "ApiVersion";
        public static string EnableBatching = "EnableBatching";
        public static string BatchSize = "BatchSize";

        // Defined within MoesifOptions.cs
        public static string IdentifyUser = "IdentifyUser";
        public static string IdentifyCompany = "IdentifyCompany";
        public static string GetSessionToken = "GetSessionToken";
        public static string GetMetadata = "GetMetadata";
        public static string GetMetadataOutgoing = "GetMetadataOutgoing";

        public static string asKey(string suffix)
        {
            return Key + ":" + suffix;
        }
    }
}

You will see our MoesifOptions class includes the code to configure the Moesif integration. We already have functions like IdentifyUser and IdentifyCompany defined and can be configured to your unique spec. Once your backend is integrated with Moesif, you’ll begin to see events roll in. Those events will look like this on the Moesif Events dashboard:

Moesif Metrics screen requests without a userID

You’ll notice that the User ID field is blank. This means that these events are not tagged to a specific user. Therefore, we cannot leverage user-specific metrics. In order for us to configure user tracking, we need to add a few lines of code to the configuration.

Step 2: Adding User Tracking to Our Moesif Configuration

In order to add user tracking to our configuration, we need to supply an IdentifyUser parameter to the middleware:

// MoesifOptions.cs
...

public static Func<HttpRequest, HttpResponse, string> IdentifyUser = (HttpRequest req, HttpResponse res) => {
            // Implement your custom logic to return user id
            return req.HttpContext?.User?.Identity?.Name;
        };

...

We accomplished this task in the previous guide, and thankfully, it shouldn’t require any customization to get working!

This IdentifyUser function will parse out the authorization field from the request you want to use to identify the user. The decoded JWT payload is available on the request via the sub property. This is the value we will use as our user identifier in Moesif.

With the code we supplied, Moesif will now be able to attach a user ID to an API request when the filter intercepts the request.

Step 3: Creating the JWT

In this specific example, I’ll be using a JWT that contains an sub in its payload. My JWT looks like this:

JWT.io

I’ve created a JWT through jwt.io. Jwt.io allows you to create a JWT and edit the fields without having to use an Identity Provider or other source to create one. I’ve added a sample id field to the payload of our generated JWT. Now, I’ll attach the JWT to the request in Postman.

Step 4: Sending a Request Through Postman

After opening up Postman, we will plug in the endpoint URL for our GET request. We will also go to the Authorization tab for the request and attach our JWT in the Token field. Also note that the Type field should be set to “Bearer”. Once the JWT is copied in, your request should look like this:

Postman with authorization Bearer Token

Now, send off the request. At this point, the request will be received by the server. Once at the server, the following will occur: the JWT id field will be parsed out using the Moesif Options’s IdentifyUser function and attached to the metric in Moesif.

Step 5: Verifying User Tracking

In Moesif, our request should be showing in the Live Event Log dashboard. Now we can see that a userID has been bound to the request.

You’ll see that the id passed in our JWT has now become the userID attached to the request we sent.

Moesif Live Event Log showing tracked user

At this point, we are now able to start leveraging user-specific metrics instead of having all of our data be anonymous. This can help to power many of the more advanced and valuable features within Moesif.

From Moesif

Other

Updated: