diff --git a/src/ScadaLink.ConfigurationDatabase/Configurations/AuditLogEntityTypeConfiguration.cs b/src/ScadaLink.ConfigurationDatabase/Configurations/AuditLogEntityTypeConfiguration.cs
new file mode 100644
index 0000000..ca914d3
--- /dev/null
+++ b/src/ScadaLink.ConfigurationDatabase/Configurations/AuditLogEntityTypeConfiguration.cs
@@ -0,0 +1,93 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using ScadaLink.Commons.Entities.Audit;
+
+namespace ScadaLink.ConfigurationDatabase.Configurations;
+
+///
+/// Maps the record to the central AuditLog table
+/// described in alog.md §4. Column lengths/types and the five named indexes are
+/// fixed by that specification — keep this in sync with the doc.
+///
+public class AuditLogEntityTypeConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("AuditLog");
+
+ builder.HasKey(e => e.EventId);
+
+ // Enum-as-string columns: bounded varchar(32) ASCII.
+ builder.Property(e => e.Channel)
+ .HasConversion()
+ .HasMaxLength(32)
+ .IsUnicode(false)
+ .IsRequired();
+
+ builder.Property(e => e.Kind)
+ .HasConversion()
+ .HasMaxLength(32)
+ .IsUnicode(false)
+ .IsRequired();
+
+ builder.Property(e => e.Status)
+ .HasConversion()
+ .HasMaxLength(32)
+ .IsUnicode(false)
+ .IsRequired();
+
+ builder.Property(e => e.ForwardState)
+ .HasConversion()
+ .HasMaxLength(32)
+ .IsUnicode(false);
+
+ // Ascii identifier columns — never carry user-supplied unicode.
+ builder.Property(e => e.SourceSiteId)
+ .HasMaxLength(64)
+ .IsUnicode(false);
+
+ builder.Property(e => e.SourceInstanceId)
+ .HasMaxLength(128)
+ .IsUnicode(false);
+
+ builder.Property(e => e.SourceScript)
+ .HasMaxLength(128)
+ .IsUnicode(false);
+
+ builder.Property(e => e.Actor)
+ .HasMaxLength(128)
+ .IsUnicode(false);
+
+ builder.Property(e => e.Target)
+ .HasMaxLength(256)
+ .IsUnicode(false);
+
+ // Bounded unicode message column.
+ builder.Property(e => e.ErrorMessage)
+ .HasMaxLength(1024);
+
+ // ErrorDetail, RequestSummary, ResponseSummary, Extra: leave as nvarchar(max).
+
+ // Indexes — names locked to alog.md §4 for reconciliation/migration discoverability.
+ builder.HasIndex(e => e.OccurredAtUtc)
+ .IsDescending(true)
+ .HasDatabaseName("IX_AuditLog_OccurredAtUtc");
+
+ builder.HasIndex(e => new { e.SourceSiteId, e.OccurredAtUtc })
+ .IsDescending(false, true)
+ .HasDatabaseName("IX_AuditLog_Site_Occurred");
+
+ builder.HasIndex(e => e.CorrelationId)
+ .HasFilter("[CorrelationId] IS NOT NULL")
+ .HasDatabaseName("IX_AuditLog_CorrelationId");
+
+ builder.HasIndex(e => new { e.Channel, e.Status, e.OccurredAtUtc })
+ .IsDescending(false, false, true)
+ .HasDatabaseName("IX_AuditLog_Channel_Status_Occurred");
+
+ builder.HasIndex(e => new { e.Target, e.OccurredAtUtc })
+ .IsDescending(false, true)
+ .HasFilter("[Target] IS NOT NULL")
+ .HasDatabaseName("IX_AuditLog_Target_Occurred");
+ }
+}
diff --git a/src/ScadaLink.ConfigurationDatabase/ScadaLinkDbContext.cs b/src/ScadaLink.ConfigurationDatabase/ScadaLinkDbContext.cs
index c1db4a7..f25118f 100644
--- a/src/ScadaLink.ConfigurationDatabase/ScadaLinkDbContext.cs
+++ b/src/ScadaLink.ConfigurationDatabase/ScadaLinkDbContext.cs
@@ -84,6 +84,7 @@ public class ScadaLinkDbContext : DbContext, IDataProtectionKeyContext
// Audit
public DbSet AuditLogEntries => Set();
+ public DbSet AuditLogs => Set();
// Data Protection Keys (for shared ASP.NET Data Protection across nodes)
public DbSet DataProtectionKeys => Set();
diff --git a/tests/ScadaLink.ConfigurationDatabase.Tests/Configurations/AuditLogEntityTypeConfigurationTests.cs b/tests/ScadaLink.ConfigurationDatabase.Tests/Configurations/AuditLogEntityTypeConfigurationTests.cs
new file mode 100644
index 0000000..b70ee0f
--- /dev/null
+++ b/tests/ScadaLink.ConfigurationDatabase.Tests/Configurations/AuditLogEntityTypeConfigurationTests.cs
@@ -0,0 +1,114 @@
+using Microsoft.EntityFrameworkCore;
+using ScadaLink.Commons.Entities.Audit;
+using ScadaLink.ConfigurationDatabase;
+using ScadaLink.ConfigurationDatabase.Configurations;
+
+namespace ScadaLink.ConfigurationDatabase.Tests.Configurations;
+
+///
+/// Schema-level tests for (#23 M1 Bundle B).
+/// Verifies that maps to the AuditLog table with the
+/// PK, property set, column types/lengths, and five named indexes specified in alog.md §4.
+/// Inspects EF model metadata via the existing in-memory SQLite test context — no
+/// database round-trips required.
+///
+public class AuditLogEntityTypeConfigurationTests : IDisposable
+{
+ private readonly ScadaLinkDbContext _context;
+
+ public AuditLogEntityTypeConfigurationTests()
+ {
+ _context = SqliteTestHelper.CreateInMemoryContext();
+ }
+
+ public void Dispose()
+ {
+ _context.Database.CloseConnection();
+ _context.Dispose();
+ }
+
+ [Fact]
+ public void Configure_MapsToAuditLogTable_WithEventIdAsPrimaryKey()
+ {
+ var entity = _context.Model.FindEntityType(typeof(AuditEvent));
+
+ Assert.NotNull(entity);
+ Assert.Equal("AuditLog", entity!.GetTableName());
+
+ var pk = entity.FindPrimaryKey();
+ Assert.NotNull(pk);
+ var pkProperty = Assert.Single(pk!.Properties);
+ Assert.Equal(nameof(AuditEvent.EventId), pkProperty.Name);
+ }
+
+ [Fact]
+ public void Configure_HasExpectedPropertyCount()
+ {
+ var entity = _context.Model.FindEntityType(typeof(AuditEvent));
+ Assert.NotNull(entity);
+
+ var properties = entity!.GetProperties()
+ .Where(p => !p.IsShadowProperty())
+ .ToList();
+
+ // AuditEvent record exposes 21 init-only properties (alog.md §4).
+ Assert.Equal(21, properties.Count);
+ }
+
+ [Fact]
+ public void Configure_FiveExpectedIndexes_WithCorrectNames()
+ {
+ var entity = _context.Model.FindEntityType(typeof(AuditEvent));
+ Assert.NotNull(entity);
+
+ var indexNames = entity!.GetIndexes()
+ .Select(i => i.GetDatabaseName())
+ .OrderBy(n => n, StringComparer.Ordinal)
+ .ToList();
+
+ var expected = new[]
+ {
+ "IX_AuditLog_Channel_Status_Occurred",
+ "IX_AuditLog_CorrelationId",
+ "IX_AuditLog_OccurredAtUtc",
+ "IX_AuditLog_Site_Occurred",
+ "IX_AuditLog_Target_Occurred",
+ };
+
+ Assert.Equal(expected, indexNames);
+ }
+
+ [Theory]
+ [InlineData(nameof(AuditEvent.Channel))]
+ [InlineData(nameof(AuditEvent.Kind))]
+ [InlineData(nameof(AuditEvent.Status))]
+ [InlineData(nameof(AuditEvent.ForwardState))]
+ public void Configure_EnumColumns_StoredAsVarchar32(string propertyName)
+ {
+ var entity = _context.Model.FindEntityType(typeof(AuditEvent));
+ Assert.NotNull(entity);
+
+ var property = entity!.FindProperty(propertyName);
+ Assert.NotNull(property);
+
+ // Enums are converted to strings (varchar(32) IsUnicode=false on SQL Server).
+ Assert.Equal(typeof(string), property!.GetProviderClrType() ?? property.ClrType);
+ Assert.Equal(32, property.GetMaxLength());
+ Assert.False(property.IsUnicode() ?? true);
+ }
+
+ [Fact]
+ public void Configure_FilteredIndexes_HaveExpectedFilters()
+ {
+ var entity = _context.Model.FindEntityType(typeof(AuditEvent));
+ Assert.NotNull(entity);
+
+ var correlationIdx = entity!.GetIndexes()
+ .Single(i => i.GetDatabaseName() == "IX_AuditLog_CorrelationId");
+ Assert.Equal("[CorrelationId] IS NOT NULL", correlationIdx.GetFilter());
+
+ var targetIdx = entity.GetIndexes()
+ .Single(i => i.GetDatabaseName() == "IX_AuditLog_Target_Occurred");
+ Assert.Equal("[Target] IS NOT NULL", targetIdx.GetFilter());
+ }
+}