GoodBooks GB5 / DMS · TAttachment Developer Reference .NET 9 · 2026-07-04
Work Streams 1 · 2 · 3

DMS TAttachment System

Three integrated capabilities: a template-driven path resolution engine, a one-time Alfresco migration program, and a bulk file ingestion tool. All share the same IStorageProvider abstraction and TATTACHMENT table.

WS1
Template Engine
Configurable folder/filename paths from 18 system tokens + dynamic context. Phase 1/2 deferred resolution.
WS2
Alfresco Migration
Discovers legacy attachments via CMIS API, downloads, saves to new storage, inserts TATTACHMENT rows.
WS3
Bulk Ingestion
Ingest files from a server folder via CSV manifest or folder-naming convention. Delegates to WS1 upload service.

ATTACHMENTOPTION values (TINYINT, CHECK constraint): 0=Alfresco · 1=FileNet · 2=Amazon S3 · 3=File Based. This is the storage discriminator — no separate column needed.

Files Created / Modified

Work Stream 1 — Framework

GB5Framework/FrameworkBLL/Attachment/AttachmentUploadService.csNEW
GB5Framework/FrameworkBLL/Attachment/AttachmentPathResolutionService.csNEW
GB5Framework/FrameworkBLL/Attachment/AttachmentPathSettings.csNEW
GB5Framework/FrameworkBLL/Attachment/TemplateResolutionEngine.csNEW
GB5Framework/FrameworkSL/Controllers/Attachment/TemplateAttachmentController.csNEW
GB5Framework/FrameworkDAL/DTO/ECM/AttachmentTemplateDTO.csNEW
GB5Framework/FrameworkDAL/DTO/ECM/TAttachmentDTO.csMODIFIED — IsPathDeferred, ContentHash
GB5Framework/FrameworkDAL/DTO/ECM/AttachmentResolutionEnvelope.csMODIFIED — ObjectHeaderTypeId, HeaderRowGuid
GB5Framework/FrameworkBLL/Attachment/AttachmentResolutionService.csMODIFIED — calls Phase 2 after BulkResolveObjectIdAsync
GB5Shared/Storage/IStorageProvider.csMODIFIED — MoveAsync added
GB5Shared/Storage/NetworkStorageProvider.csMODIFIED — MoveAsync implemented
GB5Shared/Storage/S3StorageProvider.csMODIFIED — MoveAsync stub
GB5Framework/FrameworkSL/Program.csMODIFIED — AttachmentPathSettings + AddGB5Storage

Work Streams 2 + 3 — DMS Module

DMS/DMSDAL/DTO/Migration/MigrationDTO.csNEW
DMS/DMSDAL/Query/Migration/AlfrescoMigrationQB.csNEW
DMS/DMSDAL/CustomCode/Migration/AlfrescoMigrationDAL.csNEW
DMS/DMSBLL/Migration/AlfrescoMigrationBLL.csNEW
DMS/DMSSL/EndPoints/Migration/StartAlfrescoMigration.csNEW
DMS/DMSSL/EndPoints/Migration/GetMigrationStatus.csNEW
DMS/DMSDAL/DTO/BulkIngestion/BulkIngestionDTO.csNEW
DMS/DMSDAL/Query/BulkIngestion/BulkIngestionQB.csNEW
DMS/DMSDAL/CustomCode/BulkIngestion/BulkIngestionDAL.csNEW
DMS/DMSBLL/BulkIngestion/BulkIngestionBLL.csNEW
DMS/DMSSL/EndPoints/BulkIngestion/StartBulkIngestion.csNEW
DMS/DMSSL/EndPoints/BulkIngestion/GetBulkIngestionStatus.csNEW
DMS/DMSSL/Program.csMODIFIED — FrameworkBLL scan + HttpClient
DMS/DMSBLL/DMSBLL.csprojMODIFIED — FrameworkBLL + FrameworkDAL refs
DB/Migrations/20260605_TATTACHMENT_PathResolution.sqlNEW
DB/Migrations/20260605_TATTACHMENT_Migration.sqlNEW
DB/Migrations/20260605_TBULK_INGESTION.sqlNEW

Token Registry

All 18 system tokens have keys prefixed with _. They are resolved by SystemTokenResolver from static context (date, login, template master data).

Token keySourceExampleNotes
_tenantCodeMCLIENT.CLIENTCODEACMESanitised
_tenantNameMCLIENT.CLIENTNAMEAcme CorpSanitised
_yearUploadedOn.Year2026
_monthUploadedOn.Month072-digit padded
_yyyymmUploadedOn202607
_yyyymmddUploadedOn20260704
_moduleCodeTemplate.ModuleCodeMMFrom SP_ATTACHMENT_LOAD_TEMPLATE
_moduleNameTemplate.ModuleNameMaterials
_entityCodeMENTITY.ENTITYCODEPURCHASEORDER
_entityNameMENTITY.ENTITYNAMEPurchase Order
_docTypeCodeTemplate.DocumentTypeCodeGRN
_docTypeNameTemplate.DocumentTypeNameGoods Receipt Note
_docSetCodeTemplate.DocumentSetCodeDS_PO_001
_slNoContext.SlNo0013-digit padded
_versionContext.Version1
_extFileExtensionpdfLeading dot stripped, lowercased
_objectIdContext.ObjectId (or RowGuid in Phase 1)12345RowGuid used when ObjectId ≤ 0
_rowGuidContext.RowGuida1b2c3d4...FE-generated UUID

Dynamic context tokens

Any key in ContextDictionary is resolved by DictionaryTokenResolver. Supports dot-notation for nested objects:

// ContextDictionary sent from FE:
{ "poNumber": "PO-2026-001", "vendor": { "vendorCode": "V042" } }

// In template:
{PONo}=poNumber|{VendorCode}=vendor.vendorCode

Template Format

Templates are pipe-separated pairs of {DisplayLabel}=resolverKey. The display label is cosmetic; only the resolver key matters.

-- Folder template (stored in MDOCUMENTSETDETAIL.LOCATIONTEMPLATE)
{Tenant}=_tenantCode|{Entity}=_entityCode|{Year}=_year|{Month}=_month|{ObjectId}=_objectId
→ Result: ACME/PURCHASEORDER/2026/07/12345/

-- File template (stored in MDOCUMENTSETDETAIL.DOCUMENTNAMETEMPLATE)
{DocType}=_docTypeCode|{SlNo}=_slNo|{Version}=_version|{Ext}=_ext
→ Result: GRN_001_1.pdf

-- Display name template (stored in MDOCUMENTSETDETAIL.DISPLAYNAMETEMPLATE)
{DocTypeName}=_docTypeName|{Date}=_yyyymmdd|{Version}=_version
→ Result: Goods Receipt Note - 20260704 - 1

Assembly rules

TypeJoin charSuffixFallback on empty segments
Folder/Trailing /"" (empty string)
Filename_File extensionattachment_{Guid}.ext
Display name - None"Attachment"

Sanitise rules

All resolved values are passed through SystemTokenResolver.Sanitise(): replaces / \ : * ? " < > | \s with _, strips leading/trailing underscores. null / empty → "_". Unresolved tokens → "_UNKNOWN_".

Phase 1 / Phase 2 Flow

Phase 1 — Upload before entity save

ObjectId=0. RowGuid used as ObjectId placeholder in folder/filename. ISPATHDEFERRED=1.

→

Entity Save

SP_RESOLVE_ATTACHMENTS_BATCH sets OBJECTID. AttachmentResolutionService calls Phase 2.

→

Phase 2 — Path resolution

SP_ATTACHMENT_GET_DEFERRED → MoveAsync → SP_ATTACHMENT_UPDATE_RESOLVED_PATH. ISPATHDEFERRED=0.

Phase 2 trigger

AttachmentResolutionEnvelope now carries ObjectHeaderTypeId and HeaderRowGuid. After BulkResolveObjectIdAsync, AttachmentResolutionService calls IAttachmentPathResolutionService.ResolvePhase2Async within the same transaction.

// In AttachmentResolutionService.ResolveAsync — after BulkResolveObjectIdAsync:
if (envelope.ObjectHeaderTypeId > 0 && envelope.HeaderRowGuid != Guid.Empty)
{
    await _pathResolution
        .ResolvePhase2Async(envelope.ObjectHeaderTypeId, envelope.HeaderRowGuid, login, tx, ct)
        .ConfigureAwait(false);
}

Resolution Priority

ComponentPriority 1Priority 2Fallback
Folder path LOCATIONTEMPLATE TAGFIELDS AttachmentPathSettings.DefaultFolderTemplate
File name DOCUMENTNAMETEMPLATE PROPERTYFIELDS AttachmentPathSettings.DefaultFileTemplate
Display name DISPLAYNAMETEMPLATE {DocTypeName} — {yyyyMMdd} — v{Version}

Default templates (appsettings / AttachmentPathSettings)

DefaultFolderTemplate: "{TenantCode}=_tenantCode|{EntityCode}=_entityCode|{Year}=_year|{Month}=_month|{ObjectId}=_objectId"
DefaultFileTemplate:   "{DocTypeCode}=_docTypeCode|{SlNo}=_slNo|{Version}=_version|{Ext}=_ext"

Upload Service

IAttachmentUploadService.UploadAsync orchestrates the full upload pipeline in 7 steps:

#StepImplementation detail
1Next SlNo + VersionSP_ATTACHMENT_GET_NEXT_SLNO_VERSION
2Build resolution contextFills AttachmentResolutionContext from request + login
3Resolve folder + filenameITemplateResolutionEngine.ResolveAsync()
4Buffer stream, MD5 + sizeCopies to MemoryStream; MD5.HashData() via stackalloc
5Save to storageIStorageProvider.SaveAsync(StorageKey, contentType)
6Resolve DocumentTypeIdSELECT DOCUMENTTYPEID FROM MDOCUMENTSETDETAIL WHERE …
7Insert TATTACHMENT rowSP_ATTACHMENT_INSERT → returns AttachmentId

Provider → ATTACHMENTOPTION mapping: StorageProviderType.S3 → 2 (AmazonS3) · StorageProviderType.Network → 3 (FileBased)

Alfresco Migration — Architecture

Key constraint: Legacy attachments do not exist in TATTACHMENT. They live exclusively in Alfresco. Migration must INSERT new rows — nothing to update.

Entity table discovery (no config table)

MENTITY already links to DBOBJECT (table name) and DBOBJECTFIELDS (PK column). The migration queries this at runtime — no TATTACHMENT_MIGRATION_CONFIG table needed.

-- EntityTableInfoDTO populated by:
SELECT
    E.ENTITYID    AS EntityId,  E.ENTITYCODE AS EntityCode,
    DO.DBOBJECTNAME  AS SourceTable,
    DF.DBFIELDNAME   AS PKColumn
FROM   DBO.MENTITY        E
JOIN   DBO.DBOBJECT       DO ON DO.DBOBJECTID = E.DBOBJECTID
JOIN   DBO.DBOBJECTFIELDS DF ON DF.DBOBJECTID = DO.DBOBJECTID
                             AND DF.ISPRIMARYKEY = 1
WHERE  E.ENTITYID = @ObjectTypeId

MCOMMONCONFIG keys

KeyDescription
AlfrescoCMSAlfresco base URL (e.g. http://alfresco:8080)
AlfrescoCMSUserIdAdmin username for Basic auth
AlfrescoCMSPasswordEncrypted password (same encryption as legacy)

Migration Flow

#StepDetail
1GetEntityTableInfoMENTITY → DBOBJECT → DBOBJECTFIELDS
2GetAlfrescoSettingsMCOMMONCONFIG keys → AlfrescoSettings record
3Page source tableSELECT [{pk}] FROM DBO.[{table}] … OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY
4IsAlreadyMigrated checkSELECT COUNT(1) FROM TATTACHMENT WHERE ATTACHMENTOPTION IN (2,3)
5DiscoverAlfrescoAttachmentsPOST CMIS SQL to /cmis/atom/query → parse Atom XML
6InsertMigrationLog (Pending)SP_MIGRATION_LOG_INSERT
7DownloadFromAlfrescoGET /alfresco/api/-default-/public/alfresco/versions/1/nodes/{nodeId}/content
8Compute MD5 + sizeBuffer to MemoryStream → MD5.HashData
9Save to IStorageProviderPath: {EntityCode}/ALF_MIGRATED/{ObjectId}/{nodeId}{ext}
10InsertTAttachmentSP_ATTACHMENT_INSERT (SlNo=1, Version=1, Remarks="Migrated from Alfresco")
11Update log (Done/Failed)SP_MIGRATION_LOG_UPDATE; continue on exception

Dry run

When DryRun=true, steps 7–10 are skipped. Log entry status → 4=Skipped. Lets admins verify file counts before committing writes.

Log status codes

ValueNameMeaning
0PendingLog row created, not yet started
1InProgressDownload/save in progress
2DoneTATTACHMENT row inserted successfully
3FailedException; ErrorMessage populated
4SkippedDryRun=true; no writes made

Migration Schema

CREATE TABLE DBO.TATTACHMENT_MIGRATION_LOG (
    MigrationLogId  BIGINT        IDENTITY PRIMARY KEY,
    ObjectTypeId    INT           NOT NULL,
    ObjectId        INT           NOT NULL,
    AlfrescoNodeId  NVARCHAR(200),
    DisplayFileName NVARCHAR(500),
    AttachmentId    INT           NULL,   -- set after successful TATTACHMENT insert
    Status          TINYINT       NOT NULL DEFAULT 0,
    ErrorMessage    NVARCHAR(2000),
    StartedOn       DATETIME,
    CompletedOn     DATETIME,
    ClientId        INT           NOT NULL
);

New columns on TATTACHMENT:

ColumnTypePurpose
ISPATHDEFERREDTINYINT NOT NULL DEFAULT 01 = Phase 1 path (RowGuid placeholder); 0 = resolved
CONTENTHASHNVARCHAR(64) NULLMD5 hex for dedup + integrity

Bulk Ingestion — Modes

Mode byteNameDetection logic
0ManifestManifestFilePath provided, or manifest.csv found at SourceFolder root
1FolderConventionNo manifest file found
2AutoCaller passes mode=2; same logic as above

Both modes delegate to IAttachmentUploadService.UploadAsync. All template resolution, storage, MD5, and TATTACHMENT insertion go through the same path as a normal upload.

Manifest CSV Format

FilePath,ObjectTypeId,ObjectId,DocumentSetDetailId,DisplayName,Tags,ContextJson
/data/attachments/grn_001.pdf,101,12345,4,Goods Receipt PO-001,GRN,"{""poNumber"":""PO-001""}"
/data/attachments/invoice.pdf,101,12345,5,Supplier Invoice,,
/data/attachments/report.xlsx,102,67890,8,Monthly Report,,
ColumnRequiredNotes
FilePathYesAbsolute server-side path to the file
ObjectTypeIdYesMENTITY.ENTITYID
ObjectIdYesEntity record PK
DocumentSetDetailIdNoDefaults to job-level DocumentSetDetailId if omitted or -1
DisplayNameNoOverrides DISPLAYNAMETEMPLATE if provided
TagsNoStored in TATTACHMENT.TAGS
ContextJsonNoJSON-serialised ContextDictionary for dynamic tokens

Folder Convention

Source folder layout: {SourceFolder}/{EntityCode}/{ObjectId}/

/attachments/
  PURCHASEORDER/
    12345/
      invoice.pdf
      grn.pdf
  SALESORDER/
    67890/
      order-confirmation.pdf

EntityCode is resolved to ObjectTypeId via SELECT ENTITYID FROM MENTITY WHERE ENTITYCODE=@Code. Unknown codes log a warning and skip that folder.

API Endpoints

FrameworkSL — Template Attachment

MethodRouteDescription
POST /Attachment/Template/Upload Multipart form-data upload. Form fields: DocumentSetDetailId, ObjectTypeId, ObjectId, BizTransactionTypeId, RowGuid, AttachmentType, Remarks, Tags, ContextDictionaryJson (JSON string).
DEL /Attachment/Template/Delete/{attachmentId} Deletes file from storage + sets TATTACHMENT.STATUS=2.

DMSSL — Migration

MethodRouteBody / Params
POST /DMS/Migration/StartAlfresco Body: { ObjectTypeId, BatchSize=100, DryRun=false }
GET /DMS/Migration/Status?objectTypeId={id} Returns MigrationStatusDTO (Total, Done, Failed, Pending, InProgress, Skipped)

DMSSL — Bulk Ingestion

MethodRouteBody / Params
POST /DMS/BulkIngestion/Start Body: { SourceFolder, ManifestFilePath?, DocumentSetDetailId=-1 }
GET /DMS/BulkIngestion/Status/{jobId} Returns BulkIngestionStatusDTO (JobId, Status, TotalFiles, ProcessedFiles, FailedFiles)

Stored Procedures

Path Resolution (migration: 20260605_TATTACHMENT_PathResolution.sql)

  • SP_ATTACHMENT_LOAD_TEMPLATEJoins MDOCUMENTSETDETAIL + MDOCUMENTSET + MDOCUMENTTYPE + MMODULE; returns AttachmentTemplateData
  • SP_ATTACHMENT_GET_NEXT_SLNO_VERSIONMAX(SLNO)+1 and MAX(VERSION)+1 scoped by ObjectId or RowGuid
  • SP_ATTACHMENT_INSERTFull TATTACHMENT insert; SCOPE_IDENTITY() returned as AttachmentId
  • SP_ATTACHMENT_GET_DEFERREDReturns ISPATHDEFERRED=1 rows for a given ObjectHeaderTypeId + HeaderRowGuid where OBJECTID > 0
  • SP_ATTACHMENT_UPDATE_RESOLVED_PATHBulk update via AttachmentPathResolutionTVP; clears ISPATHDEFERRED=0
  • SP_DOCUMENTSET_VALIDATE_TEMPLATEConfig-time uniqueness warnings for template conflicts

Migration (migration: 20260605_TATTACHMENT_Migration.sql)

  • SP_MIGRATION_LOG_INSERTInserts a TATTACHMENT_MIGRATION_LOG row (Status=0=Pending); returns SCOPE_IDENTITY()
  • SP_MIGRATION_LOG_UPDATEUpdates Status, AttachmentId, ErrorMessage, CompletedOn
  • SP_MIGRATION_STATUSAggregated counts (Total, Done, Failed, Pending, InProgress, Skipped) by ObjectTypeId

Bulk Ingestion (migration: 20260605_TBULK_INGESTION.sql)

  • SP_INGESTION_JOB_INSERTInserts TBULK_INGESTION_JOB; returns JobId
  • SP_INGESTION_JOB_UPDATEUpdates Status, TotalFiles, ProcessedFiles, FailedFiles, CompletedOn
  • SP_INGESTION_JOB_GETReturns BulkIngestionStatusDTO for a JobId
  • SP_INGESTION_LOG_INSERTInserts TBULK_INGESTION_LOG row (per-file result)

DI Registration

FrameworkSL/Program.cs

// Attachment path settings + storage provider
builder.Services.Configure<AttachmentPathSettings>(
    builder.Configuration.GetSection(AttachmentPathSettings.Section));
builder.Services.AddGB5Storage(
    builder.Configuration.GetSection("StorageConfiguration"));

// IAttachmentUploadService, ITemplateResolutionEngine,
// IAttachmentPathResolutionService auto-registered via assembly scan

DMSSL/Program.cs

// Named HttpClient for Alfresco downloads (10-min timeout)
builder.Services.AddHttpClient("alfresco", client => {
    client.Timeout = TimeSpan.FromMinutes(10);
});

// Attachment settings (used by BulkIngestionBLL → IAttachmentUploadService)
builder.Services.Configure<AttachmentPathSettings>(
    builder.Configuration.GetSection(AttachmentPathSettings.Section));

// Assembly scan includes FrameworkBLL + FrameworkDAL for IAttachmentUploadService etc.
var dmsBLLAssembly       = Assembly.Load("DMSBLL");
var dmsDALAssembly       = Assembly.Load("DMSDAL");
var frameworkBLLAssembly = Assembly.Load("FrameworkBLL");
var frameworkDALAssembly = Assembly.Load("FrameworkDAL");

builder.Services.Scan(scan => scan
    .FromAssemblies(dmsBLLAssembly, dmsDALAssembly,
                    frameworkBLLAssembly, frameworkDALAssembly)
    .AddClasses(c => c.Where(t => !t.IsAbstract && !t.IsInterface))
    .AsImplementedInterfaces()
    .WithScopedLifetime());

StorageConfiguration (appsettings / Vault)

"StorageConfiguration": {
    "Provider": "Network",          // or "S3"
    "NetworkBasePath": "/mnt/attachments"
}