DDD and File Management
Asked Answered
M

1

8

I've got a simple document management system I'm putting together, I'm trying to follow solid DDD principals and things have been coming together. One area I've been questioning is what the cleanest solution for managing files would be.

For background, a lot of this document management is going to revolve around uploading documents and assigning them to specific "work orders". This is in the manufacturing industry, we need to keep track of certain documents and send them to the customer when we're all done making their stuff.

So my bounded context is mostly composed of a couple main entities, lets say DocumentPackage, Document, Requirements, etc. A DocumentPackage is a grouping of documents for a single "work order". The same document may be used in multiple DocumentPackages. Each DocumentPackage has a certain number of Requirements, which is a distinct type of document that is needed as part of the package.

So when it comes to the action of uploading and downloading the files, manipulating the files, and updating the database to reflect these changes, where do I want to do most handle most of that?

Here's an example of a UploadDocumentCommand and Handler I have. Note that I decided to save the uploaded file to the local file system in the API controller, and to pass it in my command as the FileInfo.

public class UploadDocumentCommand : IRequest<AppResponse>
{
    public UploadDocumentCommand(Guid documentId, string workOrderNumber, FileInfo file, Guid? requirementId = null)
    {
        DocumentId = documentId;
        WorkOrderNumber = new WorkOrderNumber(workOrderNumber);
        FileInfo = file;
        RequirementId = requirementId;
    }

    public Guid DocumentId { get; }
    public WorkOrderNumber WorkOrderNumber { get; }
    public Guid? RequirementId { get; }
    public FileInfo FileInfo { get; }
}

public class UploadDocumentCommandHandler : IRequestHandler<UploadDocumentCommand, AppResponse>
{
    private readonly IDocumentRepository documentRepository;
    private readonly IDocumentPackageRepository packageRepo;

    public UploadDocumentCommandHandler(IDocumentRepository documentRepository, IDocumentPackageRepository packageRepo)
    {
        this.documentRepository = documentRepository;
        this.packageRepo = packageRepo;
    }
    public async Task<AppResponse> Handle(UploadDocumentCommand request, CancellationToken cancellationToken)
    {
        try
        {
            // get the package, throws not found exception if does not exist
            var package = await packageRepo.Get(request.WorkOrderNumber);

            var document = DocumentFactory.CreateFromFile(request.DocumentId, request.FileInfo);

            if (request.RequirementId != null)
            {
                package.AssignDocumentToRequirement(document, request.RequirementId.GetValueOrDefault());
            } 
            else
            {
                package.AddUnassignedDocument(document);
            }

            await documentRepository.Add(document, request.FileInfo);
            await packageRepo.Save(package);

            return AppResponse.Ok();
        }
        catch (AppException ex)
        {
            // the file may have been uploaded to docuware but failed afterwards, this should be addressed
            // this can be better handled by using an event to add the document to the package only after successful upload
            return AppResponse.Exception(ex); 
        }

    }
}

public class Document : Entity<Guid>
{
    private Document() { }
    public Document(Guid id, string fileName, DateTime addedOn)
    {
        Id = id;
        FileName = new FileName(fileName);
        AddedOn = addedOn;
    }
    public FileName FileName { get; private set; }
    public DateTime AddedOn { get; private set; }

    public override string ToString() => $"Document {Id} {FileName}";
}

My DocumentRepository has mixed responsibilities, and I'm having it both save the file to the file store as well as update the database. I'm using a specific document storage application right now, but I wanted to keep this abstracted so that I am not stuck on this. It is also possible that different files, like images, might have different stores. But part of me is wondering if it is actually better to have this logic in my application layer, where my handler takes care of storing the file and updating the database. I don't feel like the DocumentRepository is very SRP, and the act of loading my Document entity shouldn't have a dependency on my DocuwareRepository.

public class DocumentRepository : IDocumentRepository
{
    private readonly DbContext context;
    private readonly IDocuwareRepository docuwareRepository;

    public DocumentRepository(DbContext context, IDocuwareRepository docuwareRepository)
    {
        this.context = context;
        this.docuwareRepository = docuwareRepository;
    }
    public async Task<Document> Get(Guid id)
    {
        return await 
            context
            .Document
            .FirstOrDefaultAsync(x => x.Id.Equals(id));
    }

    public async Task Add(Document document, FileInfo fileInfo)
    {
        var results = await docuwareRepository.UploadToDocuware(document.Id, fileInfo);


        var storageInfo = new DocumentStorageInfo
        {
            DocuwareId = results.DocuwareDocumentId,
            DocuwareFileCabinetId = results.CabinetId,
            Document = document

        };

        context.DocumentStorageInfo.Add(storageInfo);
        context.Document.Add(document);

        await context.SaveChangesAsync();
    }

    public Task<FileStream> Download(Guid id)
    {
        throw new NotImplementedException();
    }

    public void Dispose()
    {
        context?.Dispose();
    }
}

I've got another use case I'm working on where the DocumentPackage has to be downloaded. I want my application to take all the valid documents from the package, compile them into a zip file, where the documents are structured in a folder hierarchy based on the Requirements, and that archive zip is going to get saved long term for traceability reasons, and the client can download that zip file. So I have another entity I'm calling the DocumentPackageArchive, its got a repository, and I'm thinking the zip file gets compiled there. This would call for the repository downloading all the files from their respective stores, compressing it as a zip, saving the zip locally on the web server, sending the zip file back to be saved for read-only keeping, and updating the database with some data about the archive. Yes, I am intentionally creating a copy of the document package as a snapshot.

So where this leaves me, I feel like the file management is happening all over the place. I'm saving some files in the web api becuase I feel like I need to save them to temp storage right when I get them from an IFormFile. I'm handling the file info in the application layer as part of the commands. And most of the heavy lifting is happening in the Infrastructure layer.

How would you recommend I approach this? I feel like those two repositories dealing with the documents need to be re-designed.

As an additional note, I am also considering coordinating some of this work through domain events. I don't think I'm ready for that yet, and it seems like that's a bit more complication then I need to be adding right now. Still, advise in that area would be also appreciated.

Musketeer answered 19/1, 2019 at 0:58 Comment(2)
well depends on all parts of the system how they function. sometimes it is betters to do a stream when other times is better as FileInfo. one of mistakes o tend to make is to stick to one way when i need to adapt to what best suits the need. ie if you need to pass it as attachment or manipulate it imho better as stream while if it is just saving and reading metadata or moving it around the structure then FileInfoConscript
Would you consider your Document entity anemic?Chops
S
0

I agree, you should definitely extract it somewhere. It's not a concern of the repository (whose job is to save and retrieve Aggregates) to upload the file but rather belongs to the application layer, mostly because uploading the file is part of a use case operation sequence. There should be some Infrastructure service (like FileService) that adheres to an interface that you can swap later if you are interested. Then, in your UploadDocumentCommandHandler, you should call this service directly to upload the file.

Sudorific answered 4/3, 2024 at 19:19 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.