using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using SmartStack.Infrastructure.Persistence.Extensions; using SmartStack.Infrastructure.Services.Search; using SmartStack.Infrastructure.Services.TimeEntryRefs; using {{ProjectName}}.Application.Common.Interfaces; using {{ProjectName}}.Infrastructure.Persistence; namespace {{ProjectName}}.Infrastructure; /// /// Dependency injection configuration for client Infrastructure layer. /// public static class DependencyInjection { /// /// Adds client-specific infrastructure services to the DI container. /// /// IMPORTANT: This must be called AFTER AddSmartStack() to ensure /// CoreDbContext is registered first — ExtensionsDbContext /// inherits SmartStackExtensionDbContext which references Core /// whitelist entities. /// /// /// configures SQL Server with the same resiliency defaults as Core /// (retry 5×, 60s timeout), stores the migrations history in the /// extensions schema, and registers the singleton /// SmartStackDbInitializer for ordered Core → Extensions migrations. /// public static IServiceCollection Add{{ProjectName}}Infrastructure( this IServiceCollection services, IConfiguration configuration) { // Register ExtensionsDbContext via the SDK helper — handles SQL Server // setup, retry, migrations history schema, and the migration orchestrator. services.AddSmartStackExtensionDbContext(configuration); // Expose the interface services.AddScoped(provider => provider.GetRequiredService()); // ── Global search over client entities (schema 'extensions') ────────────── // Register each list entity you want surfaced by the platform's global search. // The SDK's generic engine discovers the entity's text columns, scopes the query // (TenantScoped() adds an explicit TenantId filter — defense in depth over the // named "Tenant" context filter — and RestrictTo(...) mirrors a row-level rule), paginates // and counts. You declare only the permission, the route and the display metadata; // these compose with the Core providers in the same global search (no UI change). // See references/global-search.md. One scaffolder/agent fills the markers below. services.AddExtensionSearch(search => { // <<< EXTENSION-SEARCH-DI BEGIN >>> // search.Entity("myentities", "My entities", "Box") // .RequirePermission("{app}.{module}.{section}.read") // .RouteTo(e => $"/{app}/{module}/{section}/{e.Id}") // .TenantScoped(); // <<< EXTENSION-SEARCH-DI END >>> }); // ── HR time-entry external imputation (client dimensions: projects, mandates…) ────── // Declare each extension entity that may RECEIVE time imputation so it becomes pickable // in the core time-entry form (validated at write time, snapshotted, reported on) — with // ZERO client frontend code. Nothing is auto-registered: imputation is an opt-in business // decision, unlike global search. The RefType key is persisted on core.hr_TimeEntries — // keep it stable across versions and unique across every registered dimension. The tenant // admin must also enable the feature (HR → Time settings → external refs, default off). // See references/time-entry-refs.md. The scaffold-time-entry-refs CLI fills the markers. services.AddExtensionTimeEntryRefs(refs => { // <<< TIME-ENTRY-REFS-DI BEGIN >>> // refs.Entity("myentity", "My entities", "FolderKanban") // .WithDisplay(x => x.Name) // .WithSubtitle(x => x.Code) // .ActiveWhen(x => x.IsActive) // .RequirePermission("{app}.{module}.{section}.read") // .TenantScoped(); // <<< TIME-ENTRY-REFS-DI END >>> }); // ── Exportable datasets (your data in the HR export wizard) ──────────────── // Register an IExportDatasetProvider and your entities appear as a SOURCE in // /hr/export, mergeable with the platform's own (hours worked, absences, // recovery, employees) on the same row of the same month — with ZERO client // frontend code, because the catalogue is permission-filtered server-side. // The four output formats, the column picker, the header renaming, the live // preview, the saved templates and the schedules all come for free. // Rows must key on a USER id (resolve HrEmployee.UserId if you key on the // employment record); identity and period columns belong to the ENGINE, never // to a dataset. The engine also resolves the wizard's targeting and hands you // the union in context.UserIds — never re-filter on context.DepartmentIds. // Gate every provider with RequiredPermission and rely on your context's query // filters — never IgnoreQueryFilters(). See references/export-datasets.md. // No scaffolder: a dataset's fields, its measures and its pivots are business // decisions a column-mapping DSL cannot express. // <<< EXPORT-DATASETS-DI BEGIN >>> // services.AddSmartStackExportDatasetProvider(); // <<< EXPORT-DATASETS-DI END >>> // ── Home-page KPIs of your applications / modules ────────────────────────── // Every application and module WITHOUT a page of its own already renders a // generic presentation page (header, description, one card per child) built // from the permission-filtered menu — you write nothing for it. Register an // INavigationStatsProvider to give that page its KPI banner: the cards then // render with ZERO client frontend code. Several providers may target the same // node (they concatenate), so this is also how you append KPIs to a CORE module. // Gate every provider with RequiredPermission and rely on your context's query // filters — never IgnoreQueryFilters(). See references/navigation-home-kpis.md. // <<< NAVIGATION-STATS-DI BEGIN >>> // services.AddSmartStackNavigationStatsProvider(); // <<< NAVIGATION-STATS-DI END >>> // ── Row-level data scopes (own/assigned lists) ───────────────────────────── // Register each entity's DataScopePolicy so the platform DataScopeRegistry // knows it. The actual row filtering is mounted in ExtensionsDbContext // (ApplyDataScopeFilter — the <<< DATA-SCOPE-FILTERS >>> markers); this DI // registration complements it. The scaffold-data-scope CLI fills BOTH marker // blocks and generates the policies. NEVER use [RequireDataScope] on an // extension controller (Core-only guard) — an out-of-scope GET {id} 404s via // the named filter. See references/data-scopes.md. // <<< DATA-SCOPE-POLICIES-DI BEGIN >>> // services.AddSingleton(MyApp.Application.Crm.Pipeline.Authorization.OrderScopePolicy.Instance); // <<< DATA-SCOPE-POLICIES-DI END >>> // ── Coded entities (system-allocated business codes) ─────────────────────── // One ICodeKeyDescriptor per entity implementing ICodedEntity: the platform's // CodedEntitySaveHandler allocates the Code atomically at insert (gapless) and // the key appears in Administration → Configuration → Code patterns. No seed, // no migration — a CodePattern DB row only ever overrides the descriptor. // Requires the ExtensionsDbContext ctor to forward IServiceProvider (shipped). // The scaffold-coded-entity CLI fills the markers; scaffold-entity's // `codedEntity` input emits the entity half (Code column + ICodedEntity). // Pair the descriptor with a hand-written ICodeUniquenessProbe via the // two-generic AddSmartStackCodeKey() when the pattern may // become label-derived (CollisionStrategy.Suffix REQUIRES it; it also powers // the SmartCodeField suggestions) — see references/coded-entities.md. // <<< CODED-ENTITY-KEYS-DI BEGIN >>> // services.AddSmartStackCodeKey(); // <<< CODED-ENTITY-KEYS-DI END >>> // One AddScoped per generated business service (I{E}Service → {E}Service): // the controllers inject the interface directly, so a missing line here is // a 500 on the FIRST request ("Unable to resolve service for type // 'I{E}Service'") — build, audits and unit tests all pass without it. // scaffold-business fills the markers on every run (idempotent union); // hand-written registrations outside the block are detected and skipped. // <<< BUSINESS-SERVICES-DI BEGIN >>> // services.AddScoped(); // <<< BUSINESS-SERVICES-DI END >>> return services; } }