You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET Core 8 Audit.NET存Azure表报错,求解决方案

解决Azure Table Storage存储Audit.NET事件时的PropertyValueTooLarge错误

问题背景

在ASP.NET Core 8 Web API中基于Audit.NET实现审计追踪,使用AzureStorageTableDataProvider存储审计事件。DEV、QA等低环境运行正常,但部署到PRODUCTION环境时触发Bad Request错误:

The property value exceeds the maximum allowed size (64KB). If the property value is a string, it is UTF-16 encoded and the maximum number of characters should be 32K or less.
RequestId:xxxxx-yyyy-xxxx-yyyy-xxxxxxxx
Time:2025-01-16T04:59:02.9744140Z
Status: 400 (Bad Request)
ErrorCode: PropertyValueTooLarge

现有代码实现

审计Middleware配置

public void AuditSetupMiddleware(IApplicationBuilder app)
{
    // Add the audit Middleware to the pipeline
    app.UseAuditMiddleware(_ => _
        .FilterByRequest(r => !r.Path.Value!.EndsWith("favicon.ico") && !r.Method.Equals(nameof(HttpMethod.Get), StringComparison.OrdinalIgnoreCase))
        .WithEventType("{verb}:{url}")
        .IncludeHeaders()
        .IncludeResponseHeaders()
        .IncludeResponseBody());
}

审计输出存储配置

public void AuditSetupOutput(IApplicationBuilder app)
{
    var options = new JsonSerializerOptions()
    {
        WriteIndented = true
    };

    Configuration.Setup()
    .JsonSystemAdapter(options)
    .UseAzureTableStorage(config => config
        .Endpoint(new Uri(_appOptions.Value.AuditTrailStorageAccountUrl?.Replace(Constants.Constant.Blob, Constants.Constant.Table)!), new ManagedIdentityCredential(_appOptions.Value.UserAssignedClientId))
        .TableName(evt => $"{_purgeEntitiesOptions.Value.TargetTableName}{DateTime.UtcNow:MMMyyyy}")
        .ClientOptions(new TableClientOptions() { Retry = { MaxRetries = 3 } })
        .EntityBuilder(builder => builder
            .PartitionKey(auditEvent => auditEvent.Environment.UserName)
            .RowKey(auditEvent => Guid.NewGuid().ToString("N"))
            .Columns(col => col
                .FromDictionary(auditEvent => new Dictionary<string, object>()
                {
                    { "EventType", auditEvent.EventType },
                    { "UserName", auditEvent.Environment.UserName },
                    { "EventDuration", auditEvent.Duration },
                    { "DataSize", auditEvent.ToJson().Length },
                    { "Data", auditEvent.ToJson().Length >= 32000 ? CompressAuditEventData(auditEvent.ToJson()): auditEvent.ToJson()}
                }))));


    // Include the trace identifier in the audit events
    var httpContextAccessor = app.ApplicationServices.GetRequiredService<IHttpContextAccessor>();
    Configuration.AddCustomAction(ActionType.OnScopeCreated, scope =>
    {
        scope.SetCustomField("TraceId", httpContextAccessor.HttpContext?.TraceIdentifier);
    });

}

压缩方法

/// <summary>
/// Compress the AuditTrail data in case characters count is about 32 K or less to fix the error produced 
/// as part of Azure Table Storage limitation: The property value exceeds the maximum allowed size (64KB). 
/// If the property value is a string, it is UTF-16 encoded and the maximum number of characters should be 32K or less.
/// </summary>
/// <param name="auditTrailUnCompressedData"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
private static string CompressAuditEventData(string auditTrailUnCompressedData)
{
    if (string.IsNullOrWhiteSpace(auditTrailUnCompressedData))
    {
        throw new ArgumentNullException(nameof(auditTrailUnCompressedData));
    }

    byte[] dataToCompress = Encoding.UTF8.GetBytes(auditTrailUnCompressedData);
    byte[] compressedData = Compress(dataToCompress);
    return Encoding.UTF8.GetString(compressedData);
}

Audit过滤器配置

internal static MvcOptions AuditSetupFilter(this MvcOptions mvcOptions)
{
    mvcOptions.AddAuditFilter(config => config
        .LogRequestIf(r => !(r.Method.Equals(nameof(HttpMethod.Get), StringComparison.OrdinalIgnoreCase)) && !((r.HttpContext.Request.HasFormContentType)))
        .WithEventType("{verb} {controller}.{action}")
        .IncludeHeaders(ctx => !ctx.ModelState.IsValid)
        .IncludeModelState()
        .IncludeResponseHeaders()
        .SerializeActionParameters()
    );

    // Ignore ActionParameters
    Audit.Core.Configuration.AddCustomAction(ActionType.OnEventSaving, scope =>
    {
        scope.GetWebApiAuditAction().ActionParameters = null;
    });

    return mvcOptions;
}

Program.cs调用

builder.Services.AddControllers(config =>
{
    config.AuditSetupFilter();  
});

问题分析与修改方案

现有压缩方案存在两个核心问题:

  1. 直接将压缩后的字节数组转UTF8字符串,可能引入无效字符,且无法保证压缩后的字符串长度符合Azure Table的限制
  2. 阈值判断使用32000字符,而Azure Table的字符串限制是UTF-16编码下32768字符(对应64KB),阈值不准确

此外,不必要的JSON格式化、过大的响应体/参数收集也会增加Audit事件的体积。

修改后的代码示例

1. 修正压缩方法(使用Base64编码)

private static string CompressAuditEventData(string auditTrailUnCompressedData)
{
    if (string.IsNullOrWhiteSpace(auditTrailUnCompressedData))
    {
        throw new ArgumentNullException(nameof(auditTrailUnCompressedData));
    }

    byte[] dataToCompress = Encoding.UTF8.GetBytes(auditTrailUnCompressedData);
    using var memoryStream = new MemoryStream();
    using var gzipStream = new GZipStream(memoryStream, CompressionLevel.Optimal);
    gzipStream.Write(dataToCompress, 0, dataToCompress.Length);
    gzipStream.Close();
    // 将压缩后的字节数组转为Base64字符串,避免无效字符且长度可控
    return Convert.ToBase64String(memoryStream.ToArray());
}

// 读取审计数据时的解压方法(可选)
private static string DecompressAuditEventData(string compressedBase64Data)
{
    if (string.IsNullOrWhiteSpace(compressedBase64Data))
    {
        throw new ArgumentNullException(nameof(compressedBase64Data));
    }

    byte[] compressedData = Convert.FromBase64String(compressedBase64Data);
    using var memoryStream = new MemoryStream(compressedData);
    using var gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress);
    using var resultStream = new MemoryStream();
    gzipStream.CopyTo(resultStream);
    return Encoding.UTF8.GetString(resultStream.ToArray());
}

2. 调整存储配置的阈值与字段

public void AuditSetupOutput(IApplicationBuilder app)
{
    var options = new JsonSerializerOptions()
    {
        WriteIndented = false // 关闭JSON缩进,减少字符串体积
    };

    Configuration.Setup()
    .JsonSystemAdapter(options)
    .UseAzureTableStorage(config => config
        .Endpoint(new Uri(_appOptions.Value.AuditTrailStorageAccountUrl?.Replace(Constants.Constant.Blob, Constants.Constant.Table)!), new ManagedIdentityCredential(_appOptions.Value.UserAssignedClientId))
        .TableName(evt => $"{_purgeEntitiesOptions.Value.TargetTableName}{DateTime.UtcNow:MMMyyyy}")
        .ClientOptions(new TableClientOptions() { Retry = { MaxRetries = 3 } })
        .EntityBuilder(builder => builder
            .PartitionKey(auditEvent => auditEvent.Environment.UserName)
            .RowKey(auditEvent => Guid.NewGuid().ToString("N"))
            .Columns(col => col
                .FromDictionary(auditEvent => 
                {
                    var auditJson = auditEvent.ToJson();
                    // 使用准确的32768字符阈值(UTF-16限制)
                    var isCompressed = auditJson.Length >= 32768;
                    var dataValue = isCompressed ? CompressAuditEventData(auditJson) : auditJson;
                    
                    return new Dictionary<string, object>()
                    {
                        { "EventType", auditEvent.EventType },
                        { "UserName", auditEvent.Environment.UserName },
                        { "EventDuration", auditEvent.Duration },
                        { "DataSize", auditJson.Length },
                        { "IsCompressed", isCompressed }, // 添加标记,方便后续读取时判断是否需要解压
                        { "Data", dataValue }
                    };
                }))));


    var httpContextAccessor = app.ApplicationServices.GetRequiredService<IHttpContextAccessor>();
    Configuration.AddCustomAction(ActionType.OnScopeCreated, scope =>
    {
        scope.SetCustomField("TraceId", httpContextAccessor.HttpContext?.TraceIdentifier);
    });

}

3. 优化审计数据收集范围

// 调整Middleware,限制响应体收集条件
public void AuditSetupMiddleware(IApplicationBuilder app)
{
    app.UseAuditMiddleware(_ => _
        .FilterByRequest(r => !r.Path.Value!.EndsWith("favicon.ico") && !r.Method.Equals(nameof(HttpMethod.Get), StringComparison.OrdinalIgnoreCase))
        .WithEventType("{verb}:{url}")
        .IncludeHeaders()
        .IncludeResponseHeaders()
        // 仅收集非成功状态或小体积的响应体
        .IncludeResponseBody(ctx => 
        {
            var response = ctx.HttpContext.Response;
            return response.StatusCode >= 400 || (response.ContentLength.HasValue && response.ContentLength.Value <= 30 * 1024);
        }));
}

// 调整Audit过滤器,减少不必要的数据收集
internal static MvcOptions AuditSetupFilter(this MvcOptions mvcOptions)
{
    mvcOptions.AddAuditFilter(config => config
        .LogRequestIf(r => !(r.Method.Equals(nameof(HttpMethod.Get), StringComparison.OrdinalIgnoreCase)) && !r.HttpContext.Request.HasFormContentType)
        .WithEventType("{verb} {controller}.{action}")
        .IncludeHeaders(ctx => !ctx.ModelState.IsValid)
        .IncludeModelState(ctx => !ctx.ModelState.IsValid) // 仅在ModelState无效时收集
        .IncludeResponseHeaders()
        .SerializeActionParameters(parameters => 
        {
            // 过滤掉大体积参数(如文件、字节数组)
            return parameters.Where(p => p.Value is not IFormFile && p.Value is not byte[]);
        })
    );

    Audit.Core.Configuration.AddCustomAction(ActionType.OnEventSaving, scope =>
    {
        scope.GetWebApiAuditAction().ActionParameters = null;
    });

    return mvcOptions;
}

内容的提问来源于stack exchange,提问作者santosh kumar patro

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 01:17:33