SoFunction
Updated on 2025-04-09

Core implements dynamic audit log function

Preface

I have been writing Go and Python recently. I haven't written C# for a long time. I feel a sense of intimacy when I come back to write C# code~

Let’s get back to the point.

In today's era of rapid digitalization, every operation can have a profound impact on the business, whether it is a simple query of data or a modification of system configuration. In this context, audit logs are not only a means to follow best practices, but also a key tool to ensure data security, improve system transparency, and promote clear ownership of responsibilities. By recording in detail who did what to do with the system when and what, the audit log helps the organization track user activity, analyze system problems, and even provide necessary clues to investigate when security incidents occur.

There are many ways to implement audit logs, but how to efficiently integrate this function without interfering with the main business logic is a major challenge for developers. This article focuses on how to learn from the design idea of ​​Aspect-Oriented Programming (AOP) to implement dynamic audit logging functions in Core applications by minimizing code intrusion. AOP allows us to enhance code functionality in a declarative manner through predefined patterns such as logging, performance statistics, and security controls without modifying the actual business logic code.

This article will guide readers from understanding the concept to specific implementation, to the final data persistence processing, especially how to use MongoDB, a powerful NoSQL database to persist audit log data. Whether you are a newbie to Core or a senior developer looking to add audit capabilities to existing projects, this article will provide comprehensive guidance from theory to practice. Through this article, you will learn how to design and implement a flexible, scalable audit log system while maintaining minimal interference to the main business logic.

Let’s start this journey step by step to explore how to integrate efficient and flexible audit logging mechanisms in Core applications and use AOP design ideas to achieve highly decoupled and dynamically enhanced system functions.

Audit log basics

Definition and use

Audit logs help track user operation behavior, data change records, and system security analysis.

Commonly used audit logs are of these types.

  • Operational Audit: Record all users' operations on the system, such as login, logout, data addition, deletion, modification and search, etc.
  • Data Audit: Record the changes of data, such as recording the values ​​before and after the modification of data.
  • Security Audit: Record security-related events, such as failed login attempts, permission changes, etc.
  • Performance Audit: Record performance data of key operations to help analyze system bottlenecks.

The code in this article takes the implementation of operational audit as an example.

Model definition & key information

Audit logs are a critical part of system security and management, and help us understand what happens in the system, when, and who triggers it. To achieve this, audit logging needs to include several key components.

  • EventIdIt is a unique identifier for each audit record. Just as everyone has a unique ID number, each audit log also has a unique EventId. This allows us to easily find and reference specific audit events.
  • EventTypeDescribes the type of event that occurred. This tells us what this record is about - whether it is a user login, data modification, permission changes, etc. By looking at EventType, we can quickly understand the core information of the record without delving into the details.
  • UserIdIt is the identity of the user who triggered the event. It is very important to log a UserId in the audit log because it helps us track who is responsible for what operations. If problems or misconduct are found, we can determine the responsible person through UserId.

Design audit log model

AuditLog class

NewClass, each field has comments, so I won’t go into details.

public class AuditLog {
  /// <summary>
  /// Event unique identifier  /// </summary>
  public string EventId { get; set; }

  /// <summary>
  /// Event type (for example: login, logout, data modification, etc.)  /// </summary>
  public string EventType { get; set; }

  /// <summary>
  /// User ID for performing the operation  /// </summary>
  public string UserId { get; set; }

  /// <summary>
  /// Username of the operation  /// </summary>
  public string Username { get; set; }

  /// <summary>
  /// Timestamp of event occurrence  /// </summary>
  public DateTime Timestamp { get; set; }

  /// <summary>
  /// User IP address  /// </summary>
  public string? IPAddress { get; set; }

  /// <summary>
  /// Name of the entity being operated  /// </summary>
  public string EntityName { get; set; }

  /// <summary>
  /// The entity identifier being operated  /// </summary>
  public string EntityId { get; set; }

  /// <summary>
  /// The data before modification can be stored in JSON format according to actual conditions  /// </summary>
  public string? OriginalValues { get; set; }

  /// <summary>
  /// The modified data can be stored in JSON format according to actual conditions  /// </summary>
  public string? CurrentValues { get; set; }

  /// <summary>
  /// The specific changes can be stored in JSON format according to the actual situation  /// </summary>
  public string? Changes { get; set; }

  /// <summary>
  /// Event description  /// </summary>
  public string? Description { get; set; }
}

Capture audit logs

IAuditLogService Interface

First write an interface to operate the audit log. Using interfaces can keep the code neat and reusable, and also facilitates the future expansion or modification of audit logging logic.

For the sake of simplicity, we have only written a record method here.

public interface IAuditLogService {
  Task LogAsync(AuditLog auditLog);
}

Then register in the dependency injection container (assuming the name of the implementation class isAuditLogService

<IAuditLogService, AuditLogService>();

This design not only keeps the code clear and concise, but also provides sufficient flexibility for possible future requirements changes (such as changing the storage method of audit logs, adding audit fields, etc.).

The specific implementation will be introduced in the subsequent data persistence section.

ActionFilter method

In Core, Action filters provide a powerful mechanism that allows us to insert custom logic before and after the action execution of the controller.

We can automatically capture user operations and data changes without modifying existing business logic code. This method makes full use of AOP's idea and achieves the minimization of code intrusion.

Create AuditLogAttribute class

Directly upload the code, inherit fromActionFilterAttributeclass, can implement the characteristics of an Action filter, whereEventTypeandEntityNameI designed it to be specified manually, and other attributes can be obtained through various methods.

public class AuditLogAttribute : ActionFilterAttribute {
  public string EventType { get; set; }
  public string EntityName { get; set; }

  public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) {
    var sp = ;
    var ctxItems = ;

    try {
      var authService = &lt;AuthService&gt;();

      // Before the operation is executed      var executedContext = await next();

      // After the operation is executed
      // Get the current user's identity information      var user = await ();

      // Construct AuditLog object      var auditLog = new AuditLog {
        EventId = ().ToString(),
        EventType = ,
        UserId = ,
        Username = ,
        Timestamp = ,
        IPAddress = GetIpAddress(),
        EntityName = ,
        EntityId = ctxItems["AuditLog_EntityId"]?.ToString() ?? "",
        OriginalValues = ctxItems["AuditLog_OriginalValues"]?.ToString(),
        CurrentValues = ctxItems["AuditLog_CurrentValues"]?.ToString(),
        Changes = ctxItems["AuditLog_Changes"]?.ToString(),
        Description = $"Operation Type:{},Entity name:{}",
      };

      var auditService = &lt;IAuditLogService&gt;();
      await (auditLog);
    } catch (Exception ex) {
      var logger = &lt;ILogger&lt;AuditLogAttribute&gt;&gt;();
      (ex, "An error occurred while logging audit information.");
    }
  }
}

Things to note

  • Exception handling: Considering that logging should not affect the execution of the main business process, exception handling logic needs to be added to ensure that even if an exception occurs during the logging process, it will not interfere with normal business logic.
  • Performance issues: Although the audit log has been recorded in the asynchronous method, the response time may be slightly delayed if the recording process of the audit log is slow. You can use batch processing and cache to write to the database asynchronously, or put record logic into background tasks and message queues.

Get the IP address

passAttributes can obtain the IP address, but if the application is deployed behind a proxy server (for example, using a load balancer), the IP address you get directly may be the address of the proxy server, not the real IP address of the client.

So I encapsulated it hereGetIpAddressmethod

private string? GetIpAddress(HttpContext httpContext) {
  // Check the X-Forwarded-For header first (when the application is deployed behind the agent)  var forwardedFor = ["X-Forwarded-For"].FirstOrDefault();
  if (!(forwardedFor)) {
    return (',').FirstOrDefault(); // May contain multiple IP addresses  }

  // If there is no X-Forwarded-For header, or you need to directly obtain the remote IP address of the connection  return ?.ToString();
}

First try fromX-Forwarded-ForGet the IP address in the request header, which is a standard HTTP header that identifies the original IP address of the client sending the request through an HTTP proxy or load balancer. If the request is not proxyed, or if you want to get the address of the proxy server, it will fall back to use

X-Forwarded-ForIt may contain multiple IP addresses (if the request is passed through multiple proxy), so the code is usedSplit(',')to handle this situation and only take the first IP address as the real IP address of the client.

How to use

After packaging, this audit function can be easily used. You only need to add a line of code to the interface to realize the audit function.

[AuditLog(EventType = nameof(SetSubTaskFeedback), EntityName = nameof(SubTask))]
[HttpPost("sub-tasks/{subId}/set-feedback")]
public async Task<ApiResponse> SetSubTaskFeedback(string subId, [FromBody] SubTaskFeedbackDto dto) {}

Manual recording method

Although using Action filters is an efficient way to automate, in some cases, more granular control over the recording of audit logs is required. At this time, you can only modify the interface code and add audit log records to the business logic.

Although this method requires direct modification of the business code, it provides maximum flexibility and control.

There is nothing special about this code, it is called directly in the interface.IAuditLogServiceofLogAsyncMethods to record the audit log.

Share data through HttpContext

Some parameters are difficult to automatically obtain in ActionFilter, which are often related to business logic. At this time, HttpContext becomes an ideal bridge.

We can store some temporary data, such as data snapshots before the operation, in, then access this data in the filter to complete the recording of the audit log. This approach not only maintains the decoupling of the code, but also allows us to flexibly share data in different parts of the application.

Is a key-value pair collection that can be used to share data over the life of a request.

In this way the code in the interface is

["AuditLog_OriginalValues"] = ;
["AuditLog_CurrentValues"] = ;
["AuditLog_Changes"] = $"Update feedback results {} -&gt; {}";

Things to note

  • Ensure business logic andAuditLogAttributeKeys used in (such asAuditLog_OriginalValues) Unique and consistent to avoid potential conflicts. Here it is best to encapsulate a class yourself to provide these consts;
  • If the business logic is abstracted to the service layer, it is necessary to injectIHttpContextAccessorOnly by accessing HttpContext, this service can be accessed through()Come and register;

Log persistence

Effective persistence of audit logs is key to ensuring long-term security and compliance.

Select a storage plan

When choosing the most suitable storage solution, you need to consider multiple factors such as the importance of the data, the frequency of query, the cost, and the complexity of maintenance.

Relational Database (RDS)

Relational databases, such as MySQL, PostgreSQL, etc., are widely recognized for their stability and maturity. They provide strict data integrity assurance and the powerful capability of complex queries, suitable for audit logs that require performing complex analytics and reports.

  • Advantages: Data structure, supports complex queries, and mature management tools.
  • Disadvantages: relatively high costs, and may require complex architectures to support large-scale data.

NoSQL database

NoSQL databases, such as MongoDB, Cassandra, etc., provide flexible data models and good scalability on the outside, and are suitable for audit logs with variable structures or huge data volumes.

  • Advantages: High scalability, flexible data model, and fast write speed.
  • Disadvantages: The query function is relatively limited and the data consistency model is weak.

File system

Directly writing audit logs to the file system is the most direct storage method, and it is suitable for scenarios where the log volume is not particularly large or the query demand is not high.

  • Advantages: simple, low-cost, and easy to migrate;
  • Disadvantages: Inconvenient query and analysis, difficult to manage a large number of log files, and limited scalability.

Each storage solution has its applicable scenarios, so which solution to choose should be considered comprehensively based on specific needs and resource conditions. For audit log systems that require fast writes and highly scalable, NoSQL databases are a good choice.

Therefore, this article chose MongoDB to record logs.

MongoDB is chosen as the storage solution for audit logs, not only because of its high performance and scalability, but also because it supports a flexible document data model, making it simple to store unstructured or semi-structured audit data.

Implement AuditLogMongoService

Using MongoDB in C# is very simple.

The nuget package that needs to be added first

dotnet add 

Just upload the code,

public class AuditLogMongoService : IAuditLogService {
  private readonly IMongoCollection<AuditLog> _auditLogs;

  public AuditLogMongoService(string connectionString, string databaseName) {
    var client = new MongoClient(connectionString);
    var database = (databaseName);
    _auditLogs = <AuditLog>("audit_logs");
  }

  public async Task LogAsync(AuditLog auditLog) {
    await _auditLogs.InsertOneAsync(auditLog);
  }
}

Prepare for connection strings & registration services

To avoid hard coding, place the connection string in the configuration file ()inside

"ConnectionStrings": {
  "Redis": "redis:6379",
  "MongoDB": "mongodb://username:password@path-to-mongo:27017"
}

Register Service

<IAuditLogService>(sp => new AuditLogMongoService(("MongoDB"), "db_name"));

Get it done ~

Deploy MongoDB

Attached the deployment method of MongoDB. I use docker here, which is very convenient

version: '3.1'

services:

  mongo:
    image: mongo:4.4.6
    restart: always
    volumes:
      - ./data:/data/db
    environment:
      MONGO_INITDB_ROOT_USERNAME: username
      MONGO_INITDB_ROOT_PASSWORD: password
    ports:
      - 27017:27017

  mongo-express:
    image: mongo-express
    restart: always
    environment:
      ME_CONFIG_MONGODB_ADMINUSERNAME: username
      ME_CONFIG_MONGODB_ADMINPASSWORD: password
      ME_CONFIG_MONGODB_URL: mongodb://username:password@mongo:27017/
    ports:
      - 8081:8081

Use docker-compose to orchestrate, mapping ports 27017 and 8081

You can use port 8081 to access the mongo-express web service

How to view logs

  • Use MongoDB Compass to view data
  • Use the mongo-express service to view data on web pages

summary

Although it is a relatively simple function, it feels quite good to use it using AOP. I have to say that AspNetCore has rich functions~

The above is the detailed content of Core's implementation of dynamic audit log function. For more information about Core's audit log, please pay attention to my other related articles!