diff --git a/ZB.MOM.WW.LocalDb/src/ZB.MOM.WW.LocalDb/DependencyInjection/LocalDbServiceCollectionExtensions.cs b/ZB.MOM.WW.LocalDb/src/ZB.MOM.WW.LocalDb/DependencyInjection/LocalDbServiceCollectionExtensions.cs
new file mode 100644
index 0000000..4595c93
--- /dev/null
+++ b/ZB.MOM.WW.LocalDb/src/ZB.MOM.WW.LocalDb/DependencyInjection/LocalDbServiceCollectionExtensions.cs
@@ -0,0 +1,42 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+using ZB.MOM.WW.LocalDb.Internal;
+
+namespace ZB.MOM.WW.LocalDb;
+
+/// DI registration for the embedded SQLite local database.
+public static class LocalDbServiceCollectionExtensions
+{
+ ///
+ /// Binds and validates from the "LocalDb" configuration section and
+ /// registers as a singleton.
+ ///
+ ///
+ /// Invoked once, on the ILocalDb, immediately after it is constructed and before it is returned to the first
+ /// caller. This is where consumers create their tables (via the passed ) and call
+ /// to opt them into replication.
+ ///
+ public static IServiceCollection AddZbLocalDb(
+ this IServiceCollection services,
+ IConfiguration configuration,
+ Action? onReady = null)
+ {
+ services.AddOptions()
+ .Bind(configuration.GetSection("LocalDb"))
+ .ValidateOnStart();
+ services.TryAddEnumerable(
+ ServiceDescriptor.Singleton, LocalDbOptionsValidator>());
+
+ services.TryAddSingleton(sp =>
+ {
+ var options = sp.GetRequiredService>().Value;
+ var db = new SqliteLocalDb(options);
+ onReady?.Invoke(db);
+ return db;
+ });
+
+ return services;
+ }
+}
diff --git a/ZB.MOM.WW.LocalDb/src/ZB.MOM.WW.LocalDb/Internal/SqliteLocalDb.cs b/ZB.MOM.WW.LocalDb/src/ZB.MOM.WW.LocalDb/Internal/SqliteLocalDb.cs
index 9c42ea8..0f4594f 100644
--- a/ZB.MOM.WW.LocalDb/src/ZB.MOM.WW.LocalDb/Internal/SqliteLocalDb.cs
+++ b/ZB.MOM.WW.LocalDb/src/ZB.MOM.WW.LocalDb/Internal/SqliteLocalDb.cs
@@ -224,6 +224,15 @@ internal sealed class SqliteLocalDb : ILocalDb, IDisposable
foreach (var (key, value) in dict)
cmd.Parameters.AddWithValue("@" + key, value ?? DBNull.Value);
return;
+ // Any other collection would silently mis-bind under the reflection fallback (its members —
+ // Count, Keys, Length … — bind as bogus @parameters), so fail loudly instead. string is exempt:
+ // it is IEnumerable but a legitimate reflection-fallback target is never a bare string anyway.
+ case System.Collections.IEnumerable when parameters is not string:
+ throw new ArgumentException(
+ "Unsupported parameters type: pass an anonymous object / POCO (public properties bind as @Name) " +
+ "or an IDictionary (entries bind as @Key). A Dictionary, Hashtable, " +
+ "array, or other collection is not supported.",
+ nameof(parameters));
default:
foreach (var prop in PropertiesOf(parameters.GetType()))
cmd.Parameters.AddWithValue("@" + prop.Name, prop.GetValue(parameters) ?? DBNull.Value);
diff --git a/ZB.MOM.WW.LocalDb/src/ZB.MOM.WW.LocalDb/LocalDbOptionsValidator.cs b/ZB.MOM.WW.LocalDb/src/ZB.MOM.WW.LocalDb/LocalDbOptionsValidator.cs
new file mode 100644
index 0000000..fd6b238
--- /dev/null
+++ b/ZB.MOM.WW.LocalDb/src/ZB.MOM.WW.LocalDb/LocalDbOptionsValidator.cs
@@ -0,0 +1,34 @@
+using Microsoft.Extensions.Options;
+
+namespace ZB.MOM.WW.LocalDb;
+
+///
+/// Validates at startup, accumulating every failure into one result so
+/// ValidateOnStart reports all problems at once. Mirrors the fail-closed rules the
+/// SqliteLocalDb constructor also enforces.
+///
+public sealed class LocalDbOptionsValidator : IValidateOptions
+{
+ // Interpolated into a PRAGMA statement by the host (pragmas cannot be parameterized), so it is allow-listed.
+ private static readonly string[] AllowedSynchronous = ["OFF", "NORMAL", "FULL", "EXTRA"];
+
+ public ValidateOptionsResult Validate(string? name, LocalDbOptions options)
+ {
+ var failures = new List();
+
+ if (string.IsNullOrWhiteSpace(options.Path))
+ failures.Add("LocalDb:Path is required.");
+
+ if (options.BusyTimeoutMs <= 0)
+ failures.Add($"LocalDb:BusyTimeoutMs must be greater than 0; got {options.BusyTimeoutMs}.");
+
+ if (!Array.Exists(AllowedSynchronous,
+ s => s.Equals(options.Synchronous, StringComparison.OrdinalIgnoreCase)))
+ failures.Add(
+ $"LocalDb:Synchronous must be one of {string.Join("/", AllowedSynchronous)}; got '{options.Synchronous}'.");
+
+ return failures.Count > 0
+ ? ValidateOptionsResult.Fail(failures)
+ : ValidateOptionsResult.Success;
+ }
+}
diff --git a/ZB.MOM.WW.LocalDb/tests/ZB.MOM.WW.LocalDb.Tests/DependencyInjectionTests.cs b/ZB.MOM.WW.LocalDb/tests/ZB.MOM.WW.LocalDb.Tests/DependencyInjectionTests.cs
new file mode 100644
index 0000000..caf65d8
--- /dev/null
+++ b/ZB.MOM.WW.LocalDb/tests/ZB.MOM.WW.LocalDb.Tests/DependencyInjectionTests.cs
@@ -0,0 +1,115 @@
+using System.Collections;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+
+namespace ZB.MOM.WW.LocalDb.Tests;
+
+public sealed class DependencyInjectionTests : IDisposable
+{
+ private readonly string _path = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".db");
+
+ public void Dispose()
+ {
+ Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
+ if (File.Exists(_path))
+ File.Delete(_path);
+ }
+
+ private IConfiguration Config() =>
+ new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary { ["LocalDb:Path"] = _path })
+ .Build();
+
+ [Fact]
+ public async Task AddZbLocalDb_ResolvesILocalDb_Singleton()
+ {
+ var services = new ServiceCollection();
+ services.AddZbLocalDb(Config());
+ await using var provider = services.BuildServiceProvider();
+
+ var a = provider.GetRequiredService();
+ var b = provider.GetRequiredService();
+ Assert.Same(a, b);
+
+ await a.ExecuteAsync("CREATE TABLE t (id INTEGER PRIMARY KEY)");
+ await a.ExecuteAsync("INSERT INTO t (id) VALUES (@id)", new { id = 1 });
+ var rows = await a.QueryAsync("SELECT COUNT(*) FROM t", r => r.GetInt64(0));
+ Assert.Equal(1L, rows[0]);
+ }
+
+ [Fact]
+ public void AddZbLocalDb_WithRegistrations_TablesRegisteredOnStart()
+ {
+ var services = new ServiceCollection();
+ services.AddZbLocalDb(Config(), db =>
+ {
+ using var conn = db.CreateConnection();
+ using var cmd = conn.CreateCommand();
+ cmd.CommandText = "CREATE TABLE widget (id INTEGER PRIMARY KEY, name TEXT)";
+ cmd.ExecuteNonQuery();
+ db.RegisterReplicated("widget");
+ });
+ using var provider = services.BuildServiceProvider();
+
+ var db = provider.GetRequiredService();
+ Assert.Contains("widget", db.ReplicatedTables.Keys);
+ }
+
+ [Fact]
+ public void Validator_EmptyPath_Fails()
+ {
+ var result = new LocalDbOptionsValidator().Validate(
+ null, new LocalDbOptions { Path = " " });
+
+ Assert.True(result.Failed);
+ Assert.Contains(result.Failures, f => f.Contains("LocalDb:Path", StringComparison.Ordinal));
+ }
+
+ [Fact]
+ public void Validator_NonPositiveBusyTimeout_Fails()
+ {
+ var result = new LocalDbOptionsValidator().Validate(
+ null, new LocalDbOptions { Path = "x.db", BusyTimeoutMs = 0 });
+
+ Assert.True(result.Failed);
+ Assert.Contains(result.Failures, f => f.Contains("BusyTimeoutMs", StringComparison.Ordinal));
+ }
+
+ [Fact]
+ public void Validator_InvalidSynchronous_Fails()
+ {
+ var result = new LocalDbOptionsValidator().Validate(
+ null, new LocalDbOptions { Path = "x.db", Synchronous = "SOMETIMES" });
+
+ Assert.True(result.Failed);
+ Assert.Contains(result.Failures, f => f.Contains("Synchronous", StringComparison.Ordinal));
+ }
+
+ [Fact]
+ public void Validator_ValidOptions_Succeeds()
+ {
+ var result = new LocalDbOptionsValidator().Validate(
+ null, new LocalDbOptions { Path = "x.db", BusyTimeoutMs = 5000, Synchronous = "normal" });
+
+ Assert.True(result.Succeeded);
+ }
+
+ [Fact]
+ public async Task Bind_NonObjectDictionary_Throws()
+ {
+ var services = new ServiceCollection();
+ services.AddZbLocalDb(Config());
+ await using var provider = services.BuildServiceProvider();
+ var db = provider.GetRequiredService();
+ await db.ExecuteAsync("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)");
+
+ var stringDict = new Dictionary { ["id"] = "1" };
+ await Assert.ThrowsAsync(() =>
+ db.ExecuteAsync("INSERT INTO t (id) VALUES (@id)", stringDict));
+
+ var hashtable = new Hashtable { ["id"] = 1 };
+ await Assert.ThrowsAsync(() =>
+ db.ExecuteAsync("INSERT INTO t (id) VALUES (@id)", hashtable));
+ }
+}