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;
}
}