using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace MS.Microservice.Persistence.EFCore.Auditing; public static class AuditModelExtensions { internal const string FieldsAnnotation = "MS:Audit:Fields"; internal const string ResourceAnnotation = "MS:Audit:Resource"; /// Opts mapped scalar properties into automatic auditing; expressions are read during model construction. public static EntityTypeBuilder AuditChanges(this EntityTypeBuilder entity, string resourceType, Expression> fields) where TEntity : class { ArgumentNullException.ThrowIfNull(fields); ArgumentException.ThrowIfNullOrWhiteSpace(resourceType); if (resourceType.Length > 96 || resourceType.Any(value => !char.IsAsciiLetterOrDigit(value) && value is not '.' and not '-' and not '_')) throw new ArgumentException("Use at most 96 ASCII letters, digits, dots, hyphens or underscores.", nameof(resourceType)); Expression body = fields.Body; if (body is UnaryExpression conversion) body = conversion.Operand; var expressions = body is NewExpression creation ? creation.Arguments : new[] { body }.AsEnumerable(); var names = expressions.Select(expression => { if (expression is UnaryExpression convert) expression = convert.Operand; if (expression is not MemberExpression member || member.Expression != fields.Parameters[0]) throw new ArgumentException("Select direct scalar properties, for example x => new { x.Status, x.Amount }.", nameof(fields)); return member.Member.Name; }).Distinct(StringComparer.Ordinal).ToArray(); if (names.Length is < 1 or > 16) throw new ArgumentException("Select 1–16 audit properties.", nameof(fields)); entity.HasAnnotation(FieldsAnnotation, string.Join('|', names)); entity.HasAnnotation(ResourceAnnotation, resourceType); return entity; } } public sealed record AuditCollectionOptions { public int MaximumRecordBytes { get; init; } = 8192; public int MaximumRecordsPerSave { get; init; } = 1000; public int MaximumBatchBytes { get; init; } = 1024 * 1024; }