| name | ebuilder-singleton-service-recommendation-patterns |
| description | Recommendation playbook for moving reusable business logic into EBSingletonService classes in eBuilder DLL projects. USE FOR: designing service boundaries, registering services with EBSingletonService, dependency injection, consuming singleton services from TaskLibraryHandler, and keeping handlers thin. DO NOT USE FOR: task YAML schema authoring in configs/task.*.yml, SQL YAML structure authoring in configs/sql.*.yml, or basic TaskLibraryHandler boilerplate (use ebuilder-library-task-csharp-patterns). |
eBuilder Singleton Service Recommendation Patterns
Purpose
Use this skill when business logic in TaskLibraryHandler becomes large, repeated, or integration-heavy and should be extracted into reusable singleton services.
This skill covers:
- service extraction criteria and boundary decisions
EBSingletonService registration patterns
- constructor injection and cross-service consumption
- handler-to-service orchestration pattern
- reliability guardrails (context, transaction, error handling)
Scope
- Primary folder:
dll/
- Companion handler skill:
.github/skills/ebuilder-library-task-csharp-patterns/SKILL.md
- Related contract points:
configs/task.*.yml activator.assembly and activator.class
Resolve Engine Source Before API Lookup
When singleton-service work depends on eBuilder engine APIs such as IDataCachingService, IDatabaseService, or framework base classes:
- Inspect the current app's
dll/Directory.Build.targets file.
- Read the
Reference/HintPath values to see which engine build the app references.
- If the hint path looks like
.../ebuilder-engine/EBuilder.Engine/bin/Debug/net10.0/EBuilder.Core.dll, use .../ebuilder-engine/ as the engine repository root.
- Inspect the actual interface and implementation there before generating code.
Do not start with a broad workspace search when the app already declares the engine source location indirectly through these references.
When To Recommend Singleton Extraction
Recommend extracting logic from handler into singleton service when one or more apply:
- same integration logic appears in multiple handlers
- external SDK/client setup is expensive and reusable
- business rules exceed simple request orchestration
- logic needs shared caching, policy checks, or composition across modules
Keep logic in handler when:
- operation is one-off and short
- extraction would add indirection without reuse value
Core Pattern
1. Define And Register Singleton Service
using ...
public interface IDbDocService
{
Task<ReadOnlyMemory<byte>> BuildDbDocumentPdfContent(EBuilderAppContext appContext, DocumentEntryDto document);
bool IsDbDocument(DocumentEntryDto document);
}
[EBSingletonService<IDbDocService, DbDocService>()]
public class DbDocService(
IAppConfigsService appConfigsService,
IDllService dllService,
ILogger<DbDocService> logger,
IDatabaseService databaseService,
Func<DataSourceType, IDatabaseProvider> getDBProvider,
ITemplateService templateService,
IPdfService pdfService
) : IDbDocService, IDisposable
{
protected readonly TimeZoneInfo targetTimeZone = TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time");
public async Task<DocumentTextNote> BuildDbDocumentRawContent(EBuilderAppContext appContext, DocumentEntryDto document)
{
var adosDocPermissionService = dllService.GetEBSingletonService<IAdosDocPermissionService>(EB.App.Name);
var (connection, transaction, dataSourceConfig, queryBuilderProvider) = databaseService.ConnectDataSource(appContext);
using (connection)
using (transaction)
{
var hasDocRights = await adosDocPermissionService.CheckDocRights(
appContext,
document.DocumentId,
connection,
transaction,
dataSourceConfig,
queryBuilderProvider
);
if (hasDocRights == 0)
{
logger.LogError("Document cannot be accessed due to missing rights.");
throw new EBResponseException("Document cannot be accessed due to missing rights.", 403);
}
var databaseProvider = getDBProvider(dataSourceConfig.Type);
try
{
transaction.Commit();
}
catch (Exception ex)
{
logger.LogError(ex, "Error occurred while building DB document raw content.");
transaction.Rollback();
throw;
}
}
}
public bool IsDbDocument(DocumentEntryDto document)
{
if (document.FileTypeIdx == appConfigsService.ConstantConfigs[EB.App.Name].GetInt(EB.Constant.DOK_DATEITYP_IDX.PDF))
{
return false;
}
return true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
}
}
}
2. Consume Singleton In TaskLibraryHandler
public override async Task<TaskLibraryHandlerResult> Execute(object parameters, List<IFormFile> files)
{
var documentIntelligenceService = GetSingletonService<IDocumentIntelligenceService>();
var result = await documentIntelligenceService.AnalyzeAsync("sample");
return new TaskLibraryHandlerResult
{
Body = new { result }
};
}
3. Consume Singleton In Other Contexts
var documentIntelligenceService = dllService.GetEBSingletonService<IDocumentIntelligenceService>(EB.App.Name);
Reliability Guardrails
- Always pass
EBuilderAppContext from caller into service methods; do not try to store ambient app context singleton state.
- Use
databaseService.ConnectDataSource(appContext) instead of re-implementing datasource connection logic.
- Apply transaction commit/rollback only around write flows.
- Throw
EBResponseException for predictable frontend-facing failures when status codes matter.
- Use logging at decision boundaries and failure points.
Anti-Patterns
- Putting all handler code into a giant singleton with no clear bounded responsibilities.
- Introducing singleton state that is request-specific or user-specific.
- Hiding task-level contract validation inside deep service layers.
- Rebuilding connection/transaction helpers that already exist in engine services.
Checklist
- Service boundary is justified by reuse/complexity.
- Service interface is focused on business capabilities, not transport concerns.
- Handler remains small and contract-focused.
- App context is passed explicitly.
- Exceptions and status semantics are intentional and consistent.