Skip to main content

Azure File Share attachment storage

Built 2026-08-04, replicating legacy's own Azure File Share-backed attachment mechanism (legacy: Logiswift.Framework.Domain.AttachmentDetails, ad-hoc Microsoft.WindowsAzure.Storage.File calls scattered across ~8 near-identical overloads) as a real, reusable storage primitive rather than porting that duplication. Scoped deliberately narrow for this first pass — see "Still open" below for what's explicitly deferred.

IAttachmentStorageService

Application/Common/Interfaces — generic file-bytes storage, knows nothing about WMS_ATTACHMENTS, EDI files, or any other consumer's metadata:

UploadAsync(folder, fileName, ...)
DownloadAsync(folder, fileName, ...)
DeleteAsync(folder, fileName, ...)

Mirrors legacy's own {AttachPath}/{folder}/{fileName} relative-path convention. Callers own generating unique, collision-free folder/fileName values — Eos uses a GUID-based file name rather than legacy's recursive collision-check (CheckAttachment).

AzureFileShareAttachmentStorageService

Infrastructure/Storage — the one implementation, using the modern Azure.Storage.Files.Shares SDK (not legacy's old Microsoft.WindowsAzure.Storage.File). Registered as a singleton (Infrastructure/DependencyInjection.cs): the ShareClient is created once, lazily, on first use and reused after — same lazy-connect shape as RabbitMqDomainEventPublisher/EmailDispatcher.

:::caution Azure-only for this pass Legacy's other two storage modes (SHARE, a UNC path, and local disk) are not implemented — every call throws if MAS_SYSCONFIG.AttachStorageLocation isn't "AZURE" or the connection settings are missing, rather than silently falling back to nothing. :::

Connection settings come from MAS_SYSCONFIG, not appsettings

Same reasoning as the Eventing Pattern page's QueueSettingsDto: legacy already stores AttachStorageLocation/AzureStorageConnection/ AzureShareName/AttachPath there, so Eos reuses that row rather than a second, Eos-only config mechanism.

Read via ISysConfigRepository.GetAttachmentStorageSettingsAsync (AttachmentStorageSettingsDto, deliberately partial — just these four fields, not EmailAttachPath/ScanUploadedFiles/the FileShareServer/User/Pass UNC-fallback fields), cached like the other Mas*/MAS_SYSCONFIG reads (1hr TTL, key Mas:SysConfig:Attachment).

note

Unlike GetQueueSettingsAsync/GetRedisSettingsAsync/ GetEmailSettingsAsync (which fetch the whole MAS_SYSCONFIG row), this one uses a narrow Select projection per the database-first schema-drift rule — MAS_SYSCONFIG is one of the wide tables that rule specifically calls out as drift-prone.

First real consumer: WMS_ATTACHMENTS, via MasterOrder

WMS_ATTACHMENTS is generically linked by (OrderType, OrderNo) in legacy, shared across every order type, not owned by any one Eos entity; MasterOrder is just its first caller (Eos has no WarehouseOrder frontend page yet).

IAttachmentRepository (Application/Common/Interfaces) / AttachmentRepository (Infrastructure/Persistence/Repositories/Wms) writes through the real legacy WMS_ManAttachment SP — confirmed by grepping its body, not guessed:

  • @ActionType=1 (insert) — @Result = SCOPE_IDENTITY(), or -2 if the attachment type requires an approval workflow (MAS_ATTACH_TYPE. ReqApproval=1) that Eos can't resolve yet.
  • @ActionType=3 (soft-delete by AttachmentId) — sets Active=0, matches the junction-table-style soft-deactivate category in the Delete Semantics page. The SP always echoes back @AttachmentId as @Result regardless of whether a row actually matched, so the repository fetches the row first instead of trusting that echo.

:::caution Not implemented @ActionType=2 (approve/reject), 4 (delete-by-order), and 610 (WIAttachmentId/EdiFileId/OrderNo/Ref/other field updates) are not implemented — every attachment type used through Eos must have ReqApproval=0, or Create hits the -2 rejection. :::

MasterOrderService ownership checks

Gates attachment access the same way as every other child grid (Units/Case Ranges/Terms/Package Lines): resolve the MasterOrder header first (GetByIdAsync's existing territory check), then operate on the child data — never trust the attachmentId path segment alone. ResolveOwnedAttachmentAsync confirms the looked-up row's (OrderNo, OrderType) actually matches the requested order before returning/deleting its content.

Matches legacy's own 15 MB upload cap (AttachmentDetails.UploadAttachment's hard-coded limit) as a Conflict.

On a rejected Create, the already-uploaded file is deleted from the share to avoid an orphaned blob — storage is secondary to the DB row here, the reverse of the Eventing Pattern page's "never fail the write over a side-channel failure" rule, since without the DB row nothing else can ever find the file.

Nested actions on MasterOrdersController

GET/POST/GET .../content/DELETE under .../masterorders/{id}/attachments, reusing MenuPermissionNames.OperationsMasterOrders — no new menu item, same as Units/Case Ranges/Terms/Package Lines.

note

Backend only — no Eos.Web UI yet, by explicit scope decision.

MAS_ATTACH_TYPE lookup/validation

Shipped 2026-08-04 — AttachTypeDto/IAttachTypeRepository/ AttachTypeRepository (Application/Modules/Mas, Infrastructure/Persistence/Repositories/Mas) is the pure-lookup pattern applied to this table: 1hr-cached ListAsync plus a GetByCodeAsync that filters the same cached list (no extra DB round trip), read via a narrow Select projection per the database-first schema-drift rule (this table has never been confirmed drift-free across clients).

AttachTypesController exposes it [Authorize]-only, same bucket as AddressTypesController/ChoiceTypesController — no admin screen, no [RequiresMenuPermission].

MasterOrderService.UploadAttachmentAsync now calls GetByCodeAsync before the Azure File Share upload (not after): an unknown AttachmentType or a ReqApproval=1 type is rejected as a clean Conflict up front, rather than only surfacing via WMS_ManAttachment's generic -2 after a file's already been uploaded and then has to be deleted again.

Still open

  • No frontend UI — upload/list/download/delete exist only as API endpoints.
  • EDI file storage narrow primitive is now built, but still has no real caller. IEdiFileRepository/EdiFileRepository (Application/Common/Interfaces, Infrastructure/Persistence/Repositories/Edi) reads/writes EDI_CO's FileName/FileDir columns through the same IAttachmentStorageService, via the real EDI_ManCO SP — legacy's other real consumer of this same Azure File Share mechanism (previously AttachmentDetails.WriteAttachmentToStream). This is deliberately just the storage plumbing, not a solution to EDI ingest: EDI ingest itself is still blocked on the open "wrap legacy, don't rewrite" decision (see IEdiMessageLogRepository's doc comment and TASKS.md) — there's no real ingest consumer wired to this primitive yet, it exists so a future IEdiIncomingAdapter has a ready-made file primitive once that decision produces its first real adapter.
  • No approval workflow, virus scanning, or geo/EDI-linkage fieldsWorkflowCode, ScanStatus/LastScanDt, Lat/Lng, EdifileId/OrderNoRef are all real WMS_ATTACHMENTS columns this pass doesn't touch.
  • Legacy's SHARE (UNC) and local-disk storage modes — not implemented; a client still running one of those modes would need to switch AttachStorageLocation to AZURE before Eos's attachment endpoints will work at all.
  • Migrating ChoiceImage/CompanyLogo/ClientLogo off their SQL varbinary columns onto this same storage service — a related but separate idea, tracked as a Future idea in TASKS.md, not part of this pass.

Reference files

IAttachmentStorageService.cs, AzureFileShareAttachmentStorageService.cs, AttachmentStorageSettingsDto.cs, ISysConfigRepository.cs/ SysConfigRepository.cs, AttachmentDto.cs, IAttachmentRepository.cs, AttachmentRepository.cs, MasterOrderService.cs (the Attachments region), MasterOrdersController.cs (the Attachments region), AttachTypeDto.cs, IAttachTypeRepository.cs/AttachTypeRepository.cs, AttachTypesController.cs, EdiFileDto.cs, IEdiFileRepository.cs, EdiFileRepository.cs.