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).
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-2if the attachment type requires an approval workflow (MAS_ATTACH_TYPE. ReqApproval=1) that Eos can't resolve yet.@ActionType=3(soft-delete byAttachmentId) — setsActive=0, matches the junction-table-style soft-deactivate category in the Delete Semantics page. The SP always echoes back@AttachmentIdas@Resultregardless 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 6–10
(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.
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/writesEDI_CO'sFileName/FileDircolumns through the sameIAttachmentStorageService, via the realEDI_ManCOSP — legacy's other real consumer of this same Azure File Share mechanism (previouslyAttachmentDetails.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 (seeIEdiMessageLogRepository's doc comment and TASKS.md) — there's no real ingest consumer wired to this primitive yet, it exists so a futureIEdiIncomingAdapterhas a ready-made file primitive once that decision produces its first real adapter. - No approval workflow, virus scanning, or geo/EDI-linkage fields —
WorkflowCode,ScanStatus/LastScanDt,Lat/Lng,EdifileId/OrderNoRefare all realWMS_ATTACHMENTScolumns 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 switchAttachStorageLocationtoAZUREbefore Eos's attachment endpoints will work at all. - Migrating
ChoiceImage/CompanyLogo/ClientLogooff their SQLvarbinarycolumns 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.