In this article, we are going to learn how to implement an Audit Trail in ASP.NET Core.

You can find the entire source code of the implementation with SQL Script Github Link

Before moving further, the question arises what Audit Trail is?

As per Wikipedia. An audit trail (also called audit log) is a security-relevant chronological record, set of records, and/or destination and source of records that provide documentary evidence of the sequence of activities that have affected at any time a specific operation, procedure, event, or device.

https://en.wikipedia.org/wiki/Audit_trail

I knew the above definition is a bit bouncer; we will log all user activity on the website after logging into the application in simple terms.

Example: – The first event user does log in to the application that’s and login event. Then he has been redirected to some other page in the application.

Then he clicks on the change password menu then we again log this event. And finally, user logouts of application we log this event also.

This information can be used to show user activity and also to trace security issue. If someone says I have not logged in to the application, my account is hacked, we are going to have an IP address of the person who has logged in.

Table of Contents

  • Getting Started
  • Project structure
  • Installing Dapper Package from NuGet Package
  • Adding Model
  • Adding Repository Folder
  • Adding Interface and Concrete Class in Repository Folder
  • Stored procedure used for Inserting and generating Dynamic table
  • Creating AuditFilter
  • Plugging AuditFilter to Log Activity
  • Logging Login Activity
  • Logging Logout Activity
  • Database and Table where Logs are Stored

Getting Started

We are going to create a new application, as shown below.

Next, we are going to set Project Name DemoAudit and location. In the last part, we will choose the .Net Core framework and ASP.NET Core Version 3.1 as the framework for application and few advanced settings for such as configuring HTTPS and enabling docker; we are not going to enable docker settings for this project.

Now finally, click on create button to create a project.

Project structure

The project structure generated according to the configuration.

After creating the project next, we are going to install the Dapper Package from NuGet Package.

Dapper – a simple object mapper for .Net
https://github.com/StackExchange/Dapper

Installing Dapper Package from NuGet Package

After adding Dapper next, we are going to add Model.

Adding Model

In the Model, we have added properties that we will log and insert into the database.

public class AuditModel
{
    public int AuditId { get; set; }
    public string Area { get; set; }
    public string ControllerName { get; set; }
    public string ActionName { get; set; }
    public string RoleId { get; set; }
    public string LangId { get; set; }
    public string IpAddress { get; set; }
    public string IsFirstLogin { get; set; }
    public string LoggedInAt { get; set; }
    public string LoggedOutAt { get; set; }
    public string LoginStatus { get; set; }
    public string PageAccessed { get; set; }
    public string SessionId { get; set; }
    public string UrlReferrer { get; set; }
    public string UserId { get; set; }
}

After adding the model next, we will add the Repository Folder to Add Interface and Concert class.

Adding Repository Folder

Inside the repository folder, I am going to add an interface and concrete class.

Adding Interface and Concrete Class in Repository Folder

In concrete class, I have written logic for inserting data into the database.

We have not created a table to store logs; we will generate it dynamically according to datewise because storing records in one table will cause a problem in querying it.

The logic for generating the table is written in the store procedure.

using System;
using System.Data;
using System.Data.SqlClient;
using Dapper;
using DemoAudit.Models;
using Microsoft.Extensions.Configuration;

namespace DemoAudit.Repository
{
    public class AuditRepository : IAuditRepository
    {
        private readonly IConfiguration _configuration;
        public AuditRepository(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        public void InsertAuditLogs(AuditModel objauditmodel)
        {
            using SqlConnection con = new SqlConnection(_configuration.GetConnectionString("AuditDatabaseConnection"));
            try
            {
                con.Open();
                var para = new DynamicParameters();
                para.Add("@UserID", objauditmodel.UserId);
                para.Add("@SessionID", objauditmodel.SessionId);
                para.Add("@IPAddress", objauditmodel.IpAddress);
                para.Add("@PageAccessed", objauditmodel.PageAccessed);
                para.Add("@LoggedInAt", objauditmodel.LoggedInAt);
                para.Add("@LoggedOutAt", objauditmodel.LoggedOutAt);
                para.Add("@LoginStatus", objauditmodel.LoginStatus);
                para.Add("@ControllerName", objauditmodel.ControllerName);
                para.Add("@ActionName", objauditmodel.ActionName);
                para.Add("@UrlReferrer", objauditmodel.UrlReferrer);
                para.Add("@Area", objauditmodel.Area);
                para.Add("@RoleId", objauditmodel.RoleId);
                para.Add("@LangId", objauditmodel.LangId);
                para.Add("@IsFirstLogin", objauditmodel.IsFirstLogin);
                con.Execute("Usp_InsertAuditLogs", para, null, 0, CommandType.StoredProcedure);
            }
            catch (Exception)
            {
                throw;
            }
        }
    }
}

Stored procedure used for Inserting and generating Dynamic table

After completing with Insert log’s part next, we need to plug it into the proper place for logging requests. For doing that, we need to create a filter that we can use.

Creating AuditFilter

We will create ActionFilterAttribute Attribute with the name AuditFilterAttribute in this filter. We will log all parameters and then pass them to the InsertAuditLogs method for saving into the database.

using System;
using DemoAudit.Helpers;
using DemoAudit.Models;
using DemoAudit.Repository;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Headers;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace DemoAudit.Filters
{
    public class AuditFilterAttribute : ActionFilterAttribute
    {
        private readonly IAuditRepository _auditRepository;
        private readonly IHttpContextAccessor _httpContextAccessor;
        public AuditFilterAttribute(IHttpContextAccessor httpContextAccessor, IAuditRepository auditRepository)
        {
            _httpContextAccessor = httpContextAccessor;
            _auditRepository = auditRepository;
        }
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var objaudit = new AuditModel(); // Getting Action Name 
            var controllerName = ((ControllerBase)filterContext.Controller)
                .ControllerContext.ActionDescriptor.ControllerName;
            var actionName = ((ControllerBase)filterContext.Controller)
                .ControllerContext.ActionDescriptor.ActionName;
            var actionDescriptorRouteValues = ((ControllerBase)filterContext.Controller)
                .ControllerContext.ActionDescriptor.RouteValues;

            if (actionDescriptorRouteValues.ContainsKey("area"))
            {
                var area = actionDescriptorRouteValues["area"];
                if (area != null)
                {
                    objaudit.Area = Convert.ToString(area);
                }
            }

            var request = filterContext.HttpContext.Request;

            if (!string.IsNullOrEmpty(Convert.ToString(filterContext.HttpContext.Session.GetInt32(AllSessionKeys.LangId))))
            {
                objaudit.LangId = Convert.ToString(filterContext.HttpContext.Session.GetInt32(AllSessionKeys.LangId));
            }
            else
            {
                objaudit.LangId = "";
            }

            if (!string.IsNullOrEmpty(Convert.ToString(filterContext.HttpContext.Session.GetInt32(AllSessionKeys.UserId))))
            {
                objaudit.UserId = Convert.ToString(filterContext.HttpContext.Session.GetInt32(AllSessionKeys.UserId));
            }
            else
            {
                objaudit.UserId = "";
            }

            if (!string.IsNullOrEmpty(Convert.ToString(filterContext.HttpContext.Session.GetInt32(AllSessionKeys.RoleId))))
            {
                objaudit.RoleId = Convert.ToString(filterContext.HttpContext.Session.GetInt32(AllSessionKeys.RoleId));
            }
            else
            {
                objaudit.RoleId = "";
            }


            if (!string.IsNullOrEmpty(Convert.ToString(filterContext.HttpContext.Session.GetString(AllSessionKeys.IsFirstLogin))))
            {
                objaudit.IsFirstLogin = Convert.ToString(filterContext.HttpContext.Session.GetString(AllSessionKeys.IsFirstLogin));
            }
            else
            {
                objaudit.IsFirstLogin = "";
            }

            objaudit.SessionId = filterContext.HttpContext.Session.Id; ; // Application SessionID // User IPAddress 
            if (_httpContextAccessor.HttpContext != null)
                objaudit.IpAddress = Convert.ToString(_httpContextAccessor.HttpContext.Connection.RemoteIpAddress);

            objaudit.PageAccessed = Convert.ToString(filterContext.HttpContext.Request.Path); // URL User Requested 

            objaudit.LoginStatus = "A";
            objaudit.ControllerName = controllerName; // ControllerName 
            objaudit.ActionName = actionName;

            RequestHeaders header = request.GetTypedHeaders();
            Uri uriReferer = header.Referer;

            if (uriReferer != null)
            {
                objaudit.UrlReferrer = header.Referer.AbsoluteUri;
            }

            _auditRepository.InsertAuditLogs(objaudit);
        }
    }
}

Now we have created AuditFilter. Next, we need to plug it in the proper place.

Plugging AuditFilter to Log Activity

Either we can register filter globally for logging all request or on the specific controller.

  1. Globally
  2. Controller wise

Filter scope Globally

For logging Globally, we need to plug it in startup class.

public void ConfigureServices(IServiceCollection services)
 {
     services.AddControllersWithViews(options =>
     {
         options.Filters.Add(typeof(AuditFilterAttribute));
     });
 }

Filter scope Controller

To apply a filter on the controller with a dependency, you need to register it in the ConfigureServices method, as shown below.

public void ConfigureServices(IServiceCollection services)
 {
    services.AddScoped<AuditFilterAttribute>();
 }

Applied filter on Dashboard Controller

using DemoAudit.Filters;
using Microsoft.AspNetCore.Mvc;

namespace DemoAudit.Controllers
{
    [ServiceFilter(typeof(AuditFilterAttribute))]
    public class DashboardController : Controller
    {
        public IActionResult Dashboard()
        {
            return View();
        }
    }
}

After plugging in Filter to log next, I have added a few controller and view for the logging demo, added Portal Controller with login action method and Dashboard Controller with Dashboard action method.

Logging Login Activity

To log User Login, I have created AuditLogin Method, which is called on Login Controller POST method.

private void AuditLogin()
        {
            var objaudit = new AuditModel();
            objaudit.RoleId = Convert.ToString(HttpContext.Session.GetInt32(AllSessionKeys.RoleId));
            objaudit.ControllerName = "Portal";
            objaudit.ActionName = "Login";
            objaudit.Area = "";
            objaudit.LoggedInAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
            if (_httpContextAccessor.HttpContext != null)
                objaudit.IpAddress = Convert.ToString(_httpContextAccessor.HttpContext.Connection.RemoteIpAddress);
            objaudit.UserId = Convert.ToString(HttpContext.Session.GetInt32(AllSessionKeys.UserId));
            objaudit.PageAccessed = "";
            objaudit.UrlReferrer = "";
            objaudit.SessionId = HttpContext.Session.Id;
            _auditRepository.InsertAuditLogs(objaudit);
        }

Calling AuditLogin Method While User Login

[HttpPost]
  [ValidateAntiForgeryToken]
  public IActionResult Login(LoginViewModel loginViewModel)
  {
      if (loginViewModel.Username =="demoaudit" && loginViewModel.Password == "demoaudit")
      {

          HttpContext.Session.SetString(AllSessionKeys.UserId, "1");
          HttpContext.Session.SetString(AllSessionKeys.UserName, "demoaudit");
          HttpContext.Session.SetString(AllSessionKeys.IsFirstLogin, "N");
          HttpContext.Session.SetInt32(AllSessionKeys.RoleId, 1);
          HttpContext.Session.SetInt32(AllSessionKeys.LangId, 1);
          AuditLogin();
          return RedirectToAction("Dashboard", "Dashboard");
      }
      return View(loginViewModel);
  }

Logging Logout Activity

I have created the AuditLogout method to Log the Logout activity called when the logout action method is called.

private void AuditLogout()
        {
            var objaudit = new AuditModel();
            objaudit.RoleId = Convert.ToString(HttpContext.Session.GetInt32(AllSessionKeys.RoleId));
            objaudit.ControllerName = "Portal";
            objaudit.ActionName = "Logout";
            objaudit.Area = "";
            objaudit.LoggedOutAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
            if (_httpContextAccessor.HttpContext != null)
                objaudit.IpAddress = Convert.ToString(_httpContextAccessor.HttpContext.Connection.RemoteIpAddress);
            objaudit.UserId = Convert.ToString(HttpContext.Session.GetInt32(AllSessionKeys.UserId));
            objaudit.PageAccessed = "";
            objaudit.UrlReferrer = "";
            objaudit.SessionId = HttpContext.Session.Id;
       
            _auditRepository.InsertAuditLogs(objaudit);
        }

Calling AuditLogout Method While User is logging out

 [HttpGet]
 public IActionResult Logout()
 {
     try
     {
         HttpContext.Session.Clear();
         AuditLogout();
         return RedirectToAction("Login", "Portal");
     }
     catch (Exception)
     {
         throw;
     }
 }

Database and Table where Logs are Stored

We have created AuditDB Database and executed the Usp_InsertAuditLogs stored procedure. When a first-time stored procedure is called, it will check the current day table in the database; if not, it will create and start inserting data.

The User Activity is stored in the table. If you IPAddress field, then it shows because we are working on localhost. If you host the application, then it will start logging the IPAddress of Accessing Users.

You can find the entire source code of the implementation with SQL Script github Link

By Saineshwar Bageri

I am Microsoft MVP | C# Corner MVP | Code Project MVP | FULL STACK .NET Developer and working on .Net Web Technology (Asp.net, Asp.net Core,.Net Core, C#, Sqlserver, MVC, Windows, Console Application, javascript, jquery, json, ORM Dapper) and also a freelance developer.