dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.21k stars 9.95k forks source link

HtmlFileResult #41914

Closed fabiomaulo closed 2 years ago

fabiomaulo commented 2 years ago

Is there an existing issue for this?

Is your feature request related to a problem? Please describe the problem.

I know everybody can download an HTML directly using the browser but not executive guys know it or won't do it. As all series of FileResult (FileStreamResult, FileContentResult, etc.) would be useful to have something as HtmlFileResult. Basically is a ViewResult with the Content-Disposition header.

Describe the solution you'd like

A quick solution without so much work is:

public class HtmlFileResult : ActionResult
    {
        private readonly string fileDownloadName;
        private readonly ViewResult viewToRender;

        public HtmlFileResult(ViewResult viewToRender, string fileDownloadName)
        {
            if (string.IsNullOrWhiteSpace(fileDownloadName))
            {
                throw new ArgumentException($"'{nameof(fileDownloadName)}' cannot be null or whitespace.", nameof(fileDownloadName));
            }
            this.viewToRender = viewToRender ?? throw new ArgumentNullException(nameof(viewToRender));
            this.fileDownloadName = fileDownloadName;
        }

        public override async Task ExecuteResultAsync(ActionContext context)
        {
            const int BufferSize = 64 * 1024;
            var viewEngine = viewToRender.ViewEngine
                ?? (IViewEngine)context.HttpContext.RequestServices.GetService(typeof(ICompositeViewEngine));
            var viewResult = viewEngine.FindView(context, viewToRender.ViewName, true);
            if (viewResult is null)
            {
                throw new InvalidOperationException($"'{viewToRender.ViewName}' does not match any available view.");
            }

            var response = context.HttpContext.Response;
            response.StatusCode = 200;
            response.ContentType = "text/html";
            var contentDisposition = new ContentDispositionHeaderValue("attachment");
            contentDisposition.SetHttpFileName(fileDownloadName);
            response.Headers.Add("Content-Disposition", contentDisposition.ToString());
            using (TextWriter textWriter = new StreamWriter(response.Body, Encoding.UTF8,
                bufferSize: BufferSize, leaveOpen: true))
            {
                IView view = viewResult.View;
                var viewContext = new ViewContext(context, view, viewToRender.ViewData, viewToRender.TempData, textWriter, new HtmlHelperOptions());
                await view.RenderAsync(viewContext);
                await textWriter.FlushAsync();
            }
        }
    }

Additional context

Usage return new HtmlFileResult(View("Downloads/MyView", model), "yourStatsReport_ForTheBoss.html");

davidfowl commented 2 years ago

I think I can understand why you want this but can you write up the end to end scenario you are trying to accomplish? What is this feature for? Building reports? Is there another common use of this?

Would it be cleaner do do something on the razor size of the world or to expose an overload of View that supports this? Is it interesting for any other type of IActionResult?

ghost commented 2 years ago

Hi @fabiomaulo. We have added the "Needs: Author Feedback" label to this issue, which indicates that we have an open question for you before we can take further action. This issue will be closed automatically in 7 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

fabiomaulo commented 2 years ago

@davidfowl if you want, it is just another common action result as "common" are FileContentResult, FileStreamResult, PhysicalFileResult, VirtualFileResult all already available in-box with aspnet, and I'm having even CsvFileResult). We have been using the HtmlFileResult, for a long time : . "Live reports": a report with behavior (graph, clicks, etc.) that can be sent via e-mail and used as a stand-alone file . Content HTML that then will be uploaded to Mailchimp or SendGrid for a marketing campaign

For the "razor-side" I have another solution to send emails with HTML content (even this since a long time and traduced to the new razor) but it is another matter because it works entirely outside the WEB (a "simple" console app consuming a queue).

javiercn commented 2 years ago

@fabiomaulo thanks for the additional details.

The additional piece that this is doing over ViewResult is setting the content disposition header, is that the case?

ghost commented 2 years ago

Hi @fabiomaulo. We have added the "Needs: Author Feedback" label to this issue, which indicates that we have an open question for you before we can take further action. This issue will be closed automatically in 7 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

fabiomaulo commented 2 years ago

@javiercn that header is exactly what the browser needs to start the download instead render the view.

fabiomaulo commented 2 years ago

@javiercn are you suggesting that a simple line setting the header before returning the ViewResult would be enough?

javiercn commented 2 years ago

@fabiomaulo Yep.

fabiomaulo commented 2 years ago

@javiercn going to try it just now

javiercn commented 2 years ago

@fabiomaulo the other part that I want to understand better, is why you are doing things this way in the first place. Seems like you are using Razor and MVC views to generate reports that are meant to be used as files/email templates, etc.

Would having a way to generate the HTML without involving a web request make things easier for you? (Generating the HTML to a file on disk and just serving that file)

fabiomaulo commented 2 years ago

@javiercn as said to send mails using Razor I have another solution. The old version is this http://fabiomaulo.blogspot.com/2011/08/parse-string-as-razor-template.html The new one is not public, so far.

These functionalities (HtmlFileResult) are part of one of our "admins sites". Our internal users have a view where they can set filters, campaigns and others informations, then they can see the result; some users can even download the result. For marketing campaing, they have to add products, special phrases, and so on.

BTW @javiercn I'm in the middle of another stuff, I'll back to you soon.

javiercn commented 2 years ago

@fabiomaulo thanks for the additional details.

There is no rush. Take your time.

ghost commented 2 years ago

Hi @fabiomaulo. We have added the "Needs: Author Feedback" label to this issue, which indicates that we have an open question for you before we can take further action. This issue will be closed automatically in 7 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

fabiomaulo commented 2 years ago

hahahahahaha I like this bot...

fabiomaulo commented 2 years ago

Don't worry guys I'll close the issue soon; it is just a nice to have.

javiercn commented 2 years ago

@fabiomaulo apologies for the bot.

It's just a way that this doesn't show up in triage for us until you get back to us. I'm still interested to know if the suggestion that we offered worked for your case.

ghost commented 2 years ago

Hi @fabiomaulo. We have added the "Needs: Author Feedback" label to this issue, which indicates that we have an open question for you before we can take further action. This issue will be closed automatically in 7 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

fabiomaulo commented 2 years ago

Ok this is another confirmation of my old (at the time of the vi editor) rule: legge del primo che passa ley del primero que pasa rule of the first guy around Somebody who knows nothing about what you are doing, and for what, give you the solution, or a better solution, than what you are working on.

The better solution proposed by @javiercn .

    public static class ViewResultExtensions
    {
        public static ViewResult AsHtmlFile(this ViewResult source, HttpResponse response, string fileDownloadName = null)
        {
            if (source is null)
            {
                throw new ArgumentNullException(nameof(source));
            }

            if (response is null)
            {
                throw new ArgumentNullException(nameof(response));
            }

            var fn = fileDownloadName ?? $"{source.ViewName}.html".Replace('/','_');
            var contentDisposition = new ContentDispositionHeaderValue("attachment");
            contentDisposition.SetHttpFileName(fn);
            response.Headers.Add("Content-Disposition", contentDisposition.ToString());
            return source;
        }
    }

The usage: return View("Downloads/Consultas", model).AsHtmlFile(Response, "yourStatsReport_ForTheBoss.html");

Less code to maintain, fewer possible bugs, elegant code, and brilliant solution. Thanks, @javiercn for the idea I didn´t see after translating the original code to netcore.