diff --git a/Directory.Packages.props b/Directory.Packages.props
index 40c0ce4c44..f0d7468fa5 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -13,13 +13,14 @@
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/src/CoreTests/document_store_usage_tests.cs b/src/CoreTests/document_store_usage_tests.cs
new file mode 100644
index 0000000000..4157bc0894
--- /dev/null
+++ b/src/CoreTests/document_store_usage_tests.cs
@@ -0,0 +1,202 @@
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using JasperFx;
+using JasperFx.Descriptors;
+using JasperFx.Events;
+using Marten;
+using Marten.Testing.Documents;
+using Marten.Testing.Harness;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Shouldly;
+using Xunit;
+
+namespace CoreTests;
+
+///
+/// Coverage for IDocumentStoreUsageSource.TryCreateUsage on Marten's
+/// . Verifies the descriptor population pass —
+/// first-class properties, flat OptionValues, code-generation child, and
+/// per-document-type mappings with their generated DDL — drives the
+/// CritterWatch Documents tab end-to-end so the operationally-interesting
+/// settings reach the monitoring console accurately.
+///
+public class document_store_usage_tests
+{
+ [Fact]
+ public async Task usage_carries_first_class_identity_properties()
+ {
+ using var host = await BuildHost(opts =>
+ {
+ opts.Connection(ConnectionSource.ConnectionString);
+ opts.DatabaseSchemaName = "doc_usage_identity";
+ opts.AutoCreateSchemaObjects = AutoCreate.None;
+ opts.Schema.For();
+ });
+
+ var usage = await GetUsageAsync(host);
+
+ usage.ShouldNotBeNull();
+ usage.SubjectUri.ShouldBe(new Uri("marten://main"));
+ usage.StoreName.ShouldBe("Main");
+ usage.DatabaseSchemaName.ShouldBe("doc_usage_identity");
+ usage.AutoCreateSchemaObjects.ShouldBe(AutoCreate.None.ToString());
+ usage.EnumStorage.ShouldNotBeNullOrEmpty();
+ usage.Version.ShouldNotBeNullOrEmpty();
+ usage.Database.ShouldNotBeNull();
+ }
+
+ [Fact]
+ public async Task usage_includes_a_descriptor_per_registered_document_mapping()
+ {
+ using var host = await BuildHost(opts =>
+ {
+ opts.Connection(ConnectionSource.ConnectionString);
+ opts.DatabaseSchemaName = "doc_usage_mappings";
+ opts.Schema.For();
+ opts.Schema.For();
+ opts.Schema.For();
+ });
+
+ var usage = await GetUsageAsync(host);
+
+ var aliases = usage.Documents.Select(d => d.Alias).ToList();
+ aliases.ShouldContain("user");
+ aliases.ShouldContain("issue");
+ aliases.ShouldContain("target");
+
+ var userMapping = usage.Documents.Single(d => d.Alias == "user");
+ userMapping.DocumentType.FullName.ShouldBe(typeof(User).FullName);
+ userMapping.DocumentType.Name.ShouldBe(nameof(User));
+ userMapping.DatabaseSchemaName.ShouldBe("doc_usage_mappings");
+ userMapping.IdStrategy.ShouldNotBeNullOrEmpty();
+ userMapping.TenancyStyle.ShouldBe("Single");
+ userMapping.DeleteStyle.ShouldBe("Remove");
+ }
+
+ [Fact]
+ public async Task mapping_descriptor_carries_concurrency_and_tenancy_overrides()
+ {
+ using var host = await BuildHost(opts =>
+ {
+ opts.Connection(ConnectionSource.ConnectionString);
+ opts.DatabaseSchemaName = "doc_usage_overrides";
+ opts.Schema.For().UseOptimisticConcurrency(true);
+ opts.Schema.For().MultiTenanted();
+ opts.Schema.For().SoftDeleted();
+ });
+
+ var usage = await GetUsageAsync(host);
+
+ usage.Documents.Single(d => d.Alias == "user").UseOptimisticConcurrency.ShouldBeTrue();
+ usage.Documents.Single(d => d.Alias == "issue").TenancyStyle.ShouldBe("Conjoined");
+ usage.Documents.Single(d => d.Alias == "target").DeleteStyle.ShouldBe("SoftDelete");
+ }
+
+ [Fact]
+ public async Task mapping_descriptor_carries_full_create_table_ddl()
+ {
+ using var host = await BuildHost(opts =>
+ {
+ opts.Connection(ConnectionSource.ConnectionString);
+ opts.DatabaseSchemaName = "doc_usage_ddl";
+ opts.Schema.For();
+ });
+
+ var usage = await GetUsageAsync(host);
+ var mapping = usage.Documents.Single(d => d.Alias == "user");
+
+ // The DDL field is the canonical "what schema gets applied" view —
+ // operators on the CritterWatch Documents tab can copy/paste this
+ // straight into a SQL console. It must contain the CREATE TABLE
+ // statement for this mapping at minimum.
+ mapping.Ddl.ShouldNotBeNullOrEmpty();
+ mapping.Ddl.ShouldContain("CREATE", Case.Insensitive);
+ mapping.Ddl.ShouldContain("doc_usage_ddl.mt_doc_user", Case.Insensitive);
+ }
+
+ [Fact]
+ public async Task usage_carries_flat_option_values_for_secondary_settings()
+ {
+ using var host = await BuildHost(opts =>
+ {
+ opts.Connection(ConnectionSource.ConnectionString);
+ opts.DatabaseSchemaName = "doc_usage_flat";
+ opts.CommandTimeout = 42;
+ opts.UpdateBatchSize = 250;
+ opts.Schema.For();
+ });
+
+ var usage = await GetUsageAsync(host);
+
+ // CommandTimeout and UpdateBatchSize are lifted onto the flat
+ // Properties bag (Cluster C). The bag is hand-populated, so each
+ // expected key must appear with the right value.
+ usage.PropertyFor(nameof(StoreOptions.CommandTimeout))!.RawValue.ShouldBe(42);
+ usage.PropertyFor(nameof(StoreOptions.UpdateBatchSize))!.RawValue.ShouldBe(250);
+
+ // Cluster H6: HiloMaxLo lifted from HiloSequenceDefaults.
+ usage.PropertyFor("HiloMaxLo").ShouldNotBeNull();
+
+ // Cluster H7: ReadSessionPreference / WriteSessionPreference lifted
+ // from MultiHostSettings.
+ usage.PropertyFor("ReadSessionPreference").ShouldNotBeNull();
+ usage.PropertyFor("WriteSessionPreference").ShouldNotBeNull();
+ }
+
+ [Fact]
+ public async Task usage_includes_code_generation_child_descriptor()
+ {
+ using var host = await BuildHost(opts =>
+ {
+ opts.Connection(ConnectionSource.ConnectionString);
+ opts.DatabaseSchemaName = "doc_usage_codegen";
+ opts.Schema.For();
+ });
+
+ var usage = await GetUsageAsync(host);
+
+ usage.CodeGeneration.ShouldNotBeNull();
+ usage.CodeGeneration.GeneratedCodeMode.ShouldNotBeNullOrEmpty();
+ }
+
+ [Fact]
+ public async Task event_store_usage_includes_global_aggregates_when_present()
+ {
+ using var host = await BuildHost(opts =>
+ {
+ opts.Connection(ConnectionSource.ConnectionString);
+ opts.DatabaseSchemaName = "doc_usage_global_aggs";
+ // GlobalAggregates lives on the internal EventGraph implementation
+ // (not on the public IEventStoreOptions surface). CoreTests has
+ // InternalsVisibleTo, so the cast is fine here.
+ opts.EventGraph.GlobalAggregates.Add(typeof(User));
+ });
+
+ var usage = await host.Services.GetRequiredService()
+ .TryCreateUsage(CancellationToken.None);
+
+ usage.ShouldNotBeNull();
+ usage.GlobalAggregates.ShouldContain(g => g.FullName == typeof(User).FullName);
+ }
+
+ private static async Task BuildHost(Action configure)
+ {
+ return await Host.CreateDefaultBuilder()
+ .ConfigureServices(services =>
+ {
+ services.AddMarten(configure);
+ })
+ .StartAsync();
+ }
+
+ private static async Task GetUsageAsync(IHost host)
+ {
+ var store = (IDocumentStoreUsageSource)host.Services.GetRequiredService();
+ var usage = await store.TryCreateUsage(CancellationToken.None);
+ usage.ShouldNotBeNull();
+ return usage!;
+ }
+}
diff --git a/src/Marten/DocumentStore.DocumentStoreUsage.cs b/src/Marten/DocumentStore.DocumentStoreUsage.cs
new file mode 100644
index 0000000000..cc436946cf
--- /dev/null
+++ b/src/Marten/DocumentStore.DocumentStoreUsage.cs
@@ -0,0 +1,146 @@
+#nullable enable
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using JasperFx.Descriptors;
+using JasperFx.Events;
+using Marten.Schema;
+using Weasel.Core.Migrations;
+
+namespace Marten;
+
+public partial class DocumentStore : IDocumentStoreUsageSource
+{
+ ///
+ /// Build a snapshot of this store for
+ /// monitoring tools (CritterWatch). Mirrors the structure of
+ /// IEventStore.TryCreateUsage on the document side: hand-built
+ /// first-class properties for the operationally-interesting bits, flat
+ /// OptionValues for the secondary settings, and a per-document-type
+ /// for each mapping that emits
+ /// schema (skips structural-typed and skip-generation mappings).
+ ///
+ async Task IDocumentStoreUsageSource.TryCreateUsage(CancellationToken token)
+ {
+ var usage = new DocumentStoreUsage(Subject, this)
+ {
+ Database = await Options.Tenancy.DescribeDatabasesAsync(token).ConfigureAwait(false),
+ StoreName = Options.StoreName,
+ DatabaseSchemaName = Options.DatabaseSchemaName,
+ AutoCreateSchemaObjects = Options.AutoCreateSchemaObjects.ToString(),
+ EnumStorage = Options.EnumStorage.ToString(),
+ };
+
+ // Code-generation snapshot — wrapped as a child descriptor (rather than
+ // flattened) so the four obsolete-on-StoreOptions properties stay
+ // cohesive even after they ride out the deprecation window.
+#pragma warning disable CS0618 // intentional: building a diagnostic snapshot of obsolete settings
+ usage.CodeGeneration = new CodeGenerationDescriptor
+ {
+ ApplicationAssembly = Options.ApplicationAssembly?.GetName().FullName,
+ SourceCodeWritingEnabled = Options.SourceCodeWritingEnabled,
+ GeneratedCodeOutputPath = Options.GeneratedCodeOutputPath,
+ GeneratedCodeMode = Options.GeneratedCodeMode.ToString(),
+ };
+#pragma warning restore CS0618
+
+ // Per-document-type mappings — Documents collection. Skip mappings that
+ // don't emit schema (structural-typed, internal-only) so the snapshot
+ // matches what an operator would see in the database.
+ foreach (var mapping in Options.Storage.DocumentMappingsWithSchema.OrderBy(x => x.Alias))
+ {
+ usage.Documents.Add(BuildMappingDescriptor(mapping));
+ }
+
+ // Flat OptionValues lifted up onto Properties bag — populated via the
+ // OptionsDescription auto-property reader through the base ctor, but
+ // we override several to coerce non-default shapes (enums-as-strings,
+ // Configured/Default masking, etc.).
+ ApplyFlatOptionValues(usage);
+
+ return usage;
+ }
+
+ private DocumentMappingDescriptor BuildMappingDescriptor(DocumentMapping mapping)
+ {
+ var ddl = WriteSchemaCreationDdl(mapping);
+
+ return new DocumentMappingDescriptor
+ {
+ DocumentType = TypeDescriptor.For(mapping.DocumentType),
+ DatabaseSchemaName = mapping.DatabaseSchemaName,
+ Alias = mapping.Alias,
+ IdStrategy = mapping.IdStrategy?.GetType().Name ?? "None",
+ TenancyStyle = mapping.TenancyStyle.ToString(),
+ DeleteStyle = mapping.DeleteStyle.ToString(),
+ UseOptimisticConcurrency = mapping.UseOptimisticConcurrency,
+ UseNumericRevisions = mapping.UseNumericRevisions,
+ SubClassCount = mapping.SubClasses.Count(),
+ PartitioningStrategy = mapping.Partitioning?.GetType().Name,
+ Ddl = ddl,
+ };
+ }
+
+ private string WriteSchemaCreationDdl(DocumentMapping mapping)
+ {
+ try
+ {
+ using var writer = new StringWriter();
+ mapping.Schema.WriteFeatureCreation(Options.Advanced.Migrator, writer);
+ return writer.ToString();
+ }
+ catch (Exception ex)
+ {
+ // Don't let a schema-generation hiccup poison the whole snapshot —
+ // worst case the operator sees an explanatory error string instead
+ // of DDL on this one mapping.
+ return $"-- Failed to generate DDL: {ex.Message}";
+ }
+ }
+
+ private void ApplyFlatOptionValues(DocumentStoreUsage usage)
+ {
+ // Cluster A: TenantIdStyle, DefaultTenantUsageEnabled, RlsTenantSessionSetting
+ usage.AddValue(nameof(Options.TenantIdStyle), Options.TenantIdStyle.ToString());
+ usage.AddValue(nameof(Options.Advanced.DefaultTenantUsageEnabled), Options.Advanced.DefaultTenantUsageEnabled);
+ usage.AddValue(
+ "RlsTenantSessionSetting",
+ Options.RlsTenantSessionSetting != null ? "Configured" : "Default");
+
+ // Cluster B: NameDataLength, ApplyChangesLockId
+ usage.AddValue(nameof(Options.NameDataLength), Options.NameDataLength);
+ usage.AddValue(nameof(Options.ApplyChangesLockId), Options.ApplyChangesLockId);
+
+ // Cluster C: CommandTimeout, UpdateBatchSize, UseStickyConnectionLifetimes
+ usage.AddValue(nameof(Options.CommandTimeout), Options.CommandTimeout);
+ usage.AddValue(nameof(Options.UpdateBatchSize), Options.UpdateBatchSize);
+ usage.AddValue(nameof(Options.UseStickyConnectionLifetimes), Options.UseStickyConnectionLifetimes);
+
+ // Cluster D: DuplicatedFieldEnumStorage (lifted from Advanced)
+ usage.AddValue("DuplicatedFieldEnumStorage", Options.Advanced.DuplicatedFieldEnumStorage.ToString());
+
+ // Cluster F: OpenTelemetryTrackConnections (flattened from OpenTelemetry child),
+ // DisableNpgsqlLogging
+ usage.AddValue("OpenTelemetryTrackConnections", Options.OpenTelemetry.TrackConnections.ToString());
+ usage.AddValue(nameof(Options.DisableNpgsqlLogging), Options.DisableNpgsqlLogging);
+
+ // Cluster H6: HiloMaxLo / HiloMaxAdvanceToNextHiAttempts (lifted from
+ // HiloSequenceDefaults). MaxLo is the canonical chunk-size knob;
+ // MaxAdvanceToNextHiAttempts bounds retry behaviour during sequence
+ // contention. SequenceName is omitted (it's a per-document override
+ // that lives on individual mappings, not the store-wide default).
+ usage.AddValue("HiloMaxLo", Options.Advanced.HiloSequenceDefaults.MaxLo);
+ usage.AddValue("HiloMaxAdvanceToNextHiAttempts", Options.Advanced.HiloSequenceDefaults.MaxAdvanceToNextHiAttempts);
+
+ // Cluster H7: ReadSessionPreference / WriteSessionPreference
+ // (lifted from MultiHostSettings)
+ usage.AddValue(
+ "ReadSessionPreference",
+ Options.Advanced.MultiHostSettings.ReadSessionPreference.ToString());
+ usage.AddValue(
+ "WriteSessionPreference",
+ Options.Advanced.MultiHostSettings.WriteSessionPreference.ToString());
+ }
+}
diff --git a/src/Marten/DocumentStore.EventStore.cs b/src/Marten/DocumentStore.EventStore.cs
index ed22bb230b..3d25b0125b 100644
--- a/src/Marten/DocumentStore.EventStore.cs
+++ b/src/Marten/DocumentStore.EventStore.cs
@@ -376,6 +376,27 @@ async Task ISubscriptionRunner.ExecuteAsync(ISubscription subscri
usage.Events.Add(descriptor);
}
+ // DCB tag-type registrations — flatten ITagTypeRegistration onto the
+ // wire-friendly TagTypeDescriptor (4 strings; configuration shape only,
+ // no row counts).
+ foreach (var registration in Options.EventGraph.TagTypes)
+ {
+ usage.TagTypes.Add(new TagTypeDescriptor
+ {
+ TagType = registration.TagType.FullName ?? registration.TagType.Name,
+ SimpleType = registration.SimpleType.FullName ?? registration.SimpleType.Name,
+ TableSuffix = registration.TableSuffix,
+ AggregateType = registration.AggregateType?.FullName,
+ });
+ }
+
+ // Aggregates that live outside the multi-tenant boundary in tenanted
+ // setups — flat list of CLR type identities.
+ foreach (var aggregateType in Options.EventGraph.GlobalAggregates)
+ {
+ usage.GlobalAggregates.Add(TypeDescriptor.For(aggregateType));
+ }
+
return usage;
}
diff --git a/src/Marten/Marten.csproj b/src/Marten/Marten.csproj
index a834104915..a7f2bd1644 100644
--- a/src/Marten/Marten.csproj
+++ b/src/Marten/Marten.csproj
@@ -37,17 +37,22 @@
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
-
-
-
-
-
-
-
+
diff --git a/src/Marten/MartenServiceCollectionExtensions.cs b/src/Marten/MartenServiceCollectionExtensions.cs
index 8388ba4bac..fe8ac63415 100644
--- a/src/Marten/MartenServiceCollectionExtensions.cs
+++ b/src/Marten/MartenServiceCollectionExtensions.cs
@@ -167,6 +167,8 @@ Func optionSource
services.AddJasperFx();
services.AddSingleton();
services.AddSingleton(s => (IEventStore)s.GetRequiredService());
+ services.AddSingleton(s =>
+ (IDocumentStoreUsageSource)s.GetRequiredService());
services.AddSingleton();
services.AddSingleton(s =>
{
@@ -290,6 +292,7 @@ public static MartenStoreExpression AddMartenStore(this IServiceCollection
services.AddSingleton>();
services.AddSingleton(s => (IEventStore)s.GetRequiredService());
+ services.AddSingleton(s => (IDocumentStoreUsageSource)s.GetRequiredService());
var stores = services
.Where(x => !x.IsKeyedService)
diff --git a/src/Marten/StoreOptions.GeneratesCode.cs b/src/Marten/StoreOptions.GeneratesCode.cs
index 7a27bf61db..cdffbe6093 100644
--- a/src/Marten/StoreOptions.GeneratesCode.cs
+++ b/src/Marten/StoreOptions.GeneratesCode.cs
@@ -6,6 +6,7 @@
using JasperFx;
using JasperFx.CodeGeneration;
using JasperFx.Core;
+using JasperFx.Descriptors;
using Marten.Internal.CodeGeneration;
using Marten.Schema;
using Microsoft.Extensions.Hosting;
@@ -46,6 +47,7 @@ public bool SourceCodeWritingEnabled
/// Root folder where generated code should be placed. By default, this is the IHostEnvironment.ContentRootPath
///
[Obsolete(PreferJasperFxMessage)]
+ [IgnoreDescription]
public string GeneratedCodeOutputPath { get; set; }
public IReadOnlyList BuildFiles()
@@ -57,6 +59,7 @@ public IReadOnlyList BuildFiles()
.ToList();
}
+ [IgnoreDescription]
GenerationRules ICodeFileCollection.Rules => CreateGenerationRules();
string ICodeFileCollection.ChildNamespace { get; } = "DocumentStorage";
diff --git a/src/Marten/StoreOptions.Registration.cs b/src/Marten/StoreOptions.Registration.cs
index b038a29ac9..915351c446 100644
--- a/src/Marten/StoreOptions.Registration.cs
+++ b/src/Marten/StoreOptions.Registration.cs
@@ -87,7 +87,7 @@ public void RegisterCompiledQueryType(Type queryType)
CompiledQueryTypes.Fill(queryType);
}
- public class MartenAssemblyScanner
+public class MartenAssemblyScanner
{
private readonly List _assemblies = new();
private readonly List> _eventMatchers = new();
diff --git a/src/Marten/StoreOptions.cs b/src/Marten/StoreOptions.cs
index b8b15304b2..b37f03165a 100644
--- a/src/Marten/StoreOptions.cs
+++ b/src/Marten/StoreOptions.cs
@@ -452,6 +452,7 @@ public ITenancy Tenancy
/// Registry of custom projection storage factories, keyed by aggregate document type.
/// Used by EF Core projections to substitute Marten document storage with DbContext-based storage.
///
+ [IgnoreDescription]
public Dictionary> CustomProjectionStorageProviders { get; } = new();
private int _applyChangesLockId = 4004;