NorthwindTraders icon indicating copy to clipboard operation
NorthwindTraders copied to clipboard

Should Http classes belong in the Application layer?

Open punkouter2021 opened this issue 6 years ago • 8 comments

I have a form and I am submitting files so I use IFormFile. But to follow the pattern I would need to put that in the application layer. How would I do this ?

public async Task<IActionResult> OnPostAsync(CreateBasketViewModel createBasketViewModel) {

        if (createBasketViewModel.Files != null)
        {

            //long size = createBasketViewModel.Files.Sum(f => f.Length);

            List<string> filePaths = new List<string>();

            foreach (IFormFile formFile in createBasketViewModel.Files)
            {
                if (formFile.Length > 0)
                {
                    string uploads = Path.Combine(iHostingEnvironment.WebRootPath, "files");
                    string filePath = Path.Combine(uploads, formFile.FileName);

                    filePaths.Add(filePath);

                    using (FileStream stream = new FileStream(filePath, FileMode.CreateNew))
                    {
                        await formFile.CopyToAsync(stream);
                    }
                }
            }
        }

        return RedirectToPage("./Index");
    }
}

public class CreateBasketViewModel
{
    //   public IEnumerable<StatCatalogItemViewModel> StatCatalogItems { get; set; }

    public List<IFormFile> Files { get; set; }

    public string Notes { get; set; }

    public string SelectedCategory { get; set; }
}`

punkouter2021 avatar Sep 24 '19 17:09 punkouter2021

Hey, I was facing the same issue recently. Putting IFormFile/IFormFileCollection in the application layer seems like the completely opposite thing as to why you choose this kind of architecture. You will simply be introducing a dependency from the asp core web framework into your application layer. I solved this by:

These extensions are placed in the UI project:

    public static class IFormFileExtensions
    {
        public static CreateProductImagesCommand ToUploadProductImagesCommand(this IFormFileCollection files, int id)
        {
            var command = new CreateProductImagesCommand();
            command.Id = id;

            foreach (var formFile in files)
            {
                var model = new ImageInputModel();

                model.Name = formFile.FileName;
                model.ContentType = formFile.ContentType;
                formFile.CopyTo(model.Stream);

                command.RawImages.Add(model);
            }

            return command;
        }
    }
    public class CreateProductImagesCommand : IRequest<ProductImagesListViewModel>
    {
        public CreateProductImagesCommand()
        {
            RawImages = new List<ImageInputModel>();
        }

        public int Id { get; set; }

        public IList<ImageInputModel> RawImages { get; set; }
    }
    public class ImageInputModel
    {
        public ImageInputModel()
        {
            Stream = new MemoryStream();
        }

        public string Name { get; set; }

        public string ContentType { get; set; }

        public Stream Stream { get; set; }
    }
        [HttpPost("{id}/Images")]
        [ProducesResponseType(StatusCodes.Status201Created)]
        [ProducesDefaultResponseType]
        public async Task<ActionResult<Bidders.Application.Products.Commands.CreateProductImages.ProductImagesListViewModel>> CreateProductImages(int id, IFormFileCollection images)
        {
            CreateProductImagesCommand command = images.ToUploadProductImagesCommand(id);

            return Ok(await Mediator.Send(command));
        }

I only added what I needed from IFormFile as properties, you can add more. Now this is an approach if you need to send just the ID(route variable) of the product and the files you want to upload. If you want, you could add a whole input model to convert to whatever command you need down the layers. I tried to keep the file upload a separate endpoint since it requires a different content type compared to all the other endpoints(multipart/form-data instead of application/json or text/plain) and it was bugging me. Maybe there is a more elegant way, but I haven't found it yet. I am still new to designing apis.

adzhazhev avatar Sep 25 '19 05:09 adzhazhev

I can't figure it out. I needed version #2 to work but with my binding it refers to the viewmodel in application and so it doesn't work... Heres wha tI got.. If you have any ideas let me know.. otherwise Ill just put http in the application layer :(

// new way #1. File copy works but doesnt do mediatr command //public async Task<ActionResult> OnPostAsync(IFormFileCollection files, int id) //{ // CreateStatFileBasketCommand command = files.ToUploadFilesCommand(id); // await Mediator.Send(createStatFileBasketCommand);//doesnt exists. wont work // return RedirectToPage("./Index"); //}

    //new way #2
    //public async Task<ActionResult> OnPostAsync(CreateStatFileBasketCommand createStatFileBasketCommand)
    //{
    //    CreateStatFileBasketCommand command =
    //        createStatFileBasketCommand.Files.ToUploadFilesCommand(createStatFileBasketCommand.Id);

    //    await Mediator.Send(createStatFileBasketCommand);

    //    return RedirectToPage("./Index");
    //}

public class CreateStatFileBasketCommand : IRequest { public CreateStatFileBasketCommand() { Files = new List<FileInputModel>(); }

    public int Id { get; set; }

    public string Notes { get; set; }

    public int? StatCategoryId { get; set; }

    //  public List<FileInputModel> Files { get; set; }

    public List<FileInputModel> Files { get; set; }

    public class Handler : IRequestHandler<CreateStatFileBasketCommand, Unit>
    {
        private readonly ISrxDbContext _context;
        private readonly IMediator _mediator;

        public Handler(ISrxDbContext context, IMediator mediator)
        {
            _context = context;
            _mediator = mediator;
        }

public static class IFormFileExtensions { public static CreateStatFileBasketCommand ToUploadFilesCommand(this IFormFileCollection files, int id) { var command = new CreateStatFileBasketCommand(); command.Id = id;

        foreach (var formFile in files)
        {
            var model = new FileInputModel();

            model.Name = formFile.FileName;
            model.ContentType = formFile.ContentType;
            formFile.CopyTo(model.Stream);

            command.Files.Add(model);
        }

        return command;
    }
}

punkouter2021 avatar Sep 26 '19 16:09 punkouter2021

   <div class="row">
            <div class="col">
                <p>Upload one or more files using this form:</p>
                  <input type="file" name="CreateStatFileBasketCommand.Files" multiple />
            </div>
        </div>

punkouter2021 avatar Sep 26 '19 16:09 punkouter2021

To be honest, I cannot understand what issues you are facing. Your explaining is a bit chaotic :D

// new way #1. File copy works but doesnt do mediatr command
public async Task OnPostAsync(IFormFileCollection files, int id)
{
 CreateStatFileBasketCommand command = files.ToUploadFilesCommand(id);
 await Mediator.Send(createStatFileBasketCommand);//doesnt exists. wont work
 return RedirectToPage("./Index");
}

This right here where you say it doesn't exist. Of course it doesn't, you are passing Mediatr "createStatFileBasketCommand" while you should be passing "command"!

adzhazhev avatar Sep 27 '19 07:09 adzhazhev

Also, you might want to take a look at this: https://www.reddit.com/r/csharp/comments/d97d24/library_for_support_json_and_files_in/

adzhazhev avatar Sep 27 '19 08:09 adzhazhev

I can send it to you if you like .. hard to explain but I was having trouble because

CreateStatFileBasketCommand contains Files which is not a IFormsCollection and therefore couldn't bind..

This code has been really helpful though.. since I am not a good coder I guess seem to quickly get lost when I have to do something new...

punkouter2021 avatar Sep 27 '19 16:09 punkouter2021

its here if you wanna see

https://1drv.ms/u/s!Al9uN-qjI2owg7Ym_roLi3w3mZ9CNg?e=vn1Jj2

punkouter2021 avatar Sep 27 '19 16:09 punkouter2021

It's been a while since this issue was opened, but I'm facing the same kind of problem. To avoid having the HTTP classes within the application layer, I created an interface to handle my files. It follows the same pattern as @adzhazhev mentioned.

However, I am wondering if there would be any way of incorporating my files within the same [FromBody] as my command. Currently, the only way I found was either to create a different endpoint for the files (which could lead to orphan files being created on the server in case of errors) or to create a custom "viewmodel" within the WebUI project.

If you guys have any thoughts, feel free to share it! :)

Thanks,

yannickrondeau avatar Nov 29 '21 01:11 yannickrondeau

Thank you for your interest in this project. This repository has been archived and is no longer actively maintained or supported. We appreciate your understanding. Feel free to explore the codebase and adapt it to your own needs if it serves as a useful reference. If you have any further questions or concerns, please refer to the README for more information.

jasontaylordev avatar Jul 01 '23 22:07 jasontaylordev