Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Results.Stream overload that takes a Func<Stream, Task> #39383

Open
davidfowl opened this issue Jan 8, 2022 · 19 comments
Open

Add Results.Stream overload that takes a Func<Stream, Task> #39383

davidfowl opened this issue Jan 8, 2022 · 19 comments
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-minimal-actions Controller-like actions for endpoint routing help candidate Indicates that the issues may be a good fit for community to help with. Requires work from eng. team
Milestone

Comments

@davidfowl
Copy link
Member

davidfowl commented Jan 8, 2022

Background and Motivation

There are scenarios where APIs want to work directly on the response stream without buffering. Today the Stream overloads assume you have a stream that you have produced that is then copied to the underlying response stream, instead, we want to provide a result type that gives you access to the underlying response stream.

Example:

https://github.com/khalidabuhakmeh/dotnet-dramameter/blob/main/DramaMeter/Program.cs#L37-L43

Another is writing a blob storage results to the http response.

Proposed API

namespace Microsoft.AspNetCore.Http
{
    public static class Results
    {
+       public IResult Stream(Func<Stream, Task> streamWriterCallback, 
+                             string? contentType = null,
+                             string? fileDownloadName = null,
+                             DateTimeOffset? lastModified = null,
+                             EntityTagHeaderValue? entityTag = null,
+                             bool enableRangeProcessing = false);
    }
}

Usage Examples

app.MapGet("/", async (HttpContext http, string? level) => {
    level ??= "low";

    if (!levels.TryGetValue(level.Trim(), out var result))
        return Results.BadRequest("invalid level");

    var image = background.CloneAs<Rgba32>();
    image.Mutate(ctx => {
        ctx.Vignette(result.color); // match the background to the intensity
        ctx.DrawImage(foreground, new Point(0, 0), 1f);
        ctx.DrawImage(result.image, new Point(0, 0), opacity: 1f);
    });
    
    http.Response.Headers.CacheControl = $"public,max-age={FromHours(24).TotalSeconds}";
    return Results.Stream(stream => image.SaveAsync(stream, PngFormat.Instance), "image/png");
});

Risks

None

@davidfowl davidfowl added api-suggestion Early API idea and discussion, it is NOT ready for implementation api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation labels Jan 8, 2022
@ghost
Copy link

ghost commented Jan 8, 2022

Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:

  • The PR contains changes to the reference-assembly that describe the API change. Or, you have included a snippet of reference-assembly-style code that illustrates the API change.
  • The PR describes the impact to users, both positive (useful new APIs) and negative (breaking changes).
  • Someone is assigned to "champion" this change in the meeting, and they understand the impact and design of the change.

@davidfowl davidfowl added area-web-frameworks feature-minimal-actions Controller-like actions for endpoint routing labels Jan 8, 2022
@pranavkm
Copy link
Contributor

pranavkm commented Jan 8, 2022

- Func<Stream, Task> streamFactory
+Func<Stream, Task> writeHttpResponseStreamAsync,

or something like that. I think we'd spoken about this API when reviewing the Results type but left it alone at that time to get feedback. Also I think we should try and keep some parity between this and the API in controllers to allow users to migrate from one to another without too much effort (in particular since it's low cost for us to add support)

@pranavkm pranavkm added this to the .NET 7 Planning milestone Jan 8, 2022
@ghost
Copy link

ghost commented Jan 8, 2022

Thanks for contacting us.

We're moving this issue to the .NET 7 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s).
If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues.
To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

@pranavkm
Copy link
Contributor

API review:

We decided to add a few more overloads during review.

public class Results
{

+ public IResult Stream(
+     Func<Stream, Task> streamWriterCallback, 
+     string? contentType = null,
+     string? fileDownloadName = null,
+     DateTimeOffset? lastModified = null,
+     EntityTagHeaderValue? entityTag = null,
+     bool enableRangeProcessing = false);

+ public IResult Stream(
+     PipeReader pipeReader, 
+     string? contentType = null,
+     string? fileDownloadName = null,
+     DateTimeOffset? lastModified = null,
+     EntityTagHeaderValue? entityTag = null,
+     bool enableRangeProcessing = false);


+ public IResult Stream(
+     Func<PipeWriter, Task> pipeWriterCallback, 
+     string? contentType = null,
+     string? fileDownloadName = null,
+     DateTimeOffset? lastModified = null,
+     EntityTagHeaderValue? entityTag = null,
+     bool enableRangeProcessing = false);


+ public IResult Bytes(
+     ReadOnlyMemory<byte> byteBuffer, 
+     string? contentType = null,
+     string? fileDownloadName = null,
+     bool enableRangeProcessing = false,
+     DateTimeOffset? lastModified = null,
+     EntityTagHeaderValue? entityTag = null);
}
public class ControllerBase
{
+  public PushFileStreamResult File(
+      Func<Stream, Task> streamWriterCallback, 
+      string? contentType = null,
+      string? fileDownloadName = null,
+      DateTimeOffset? lastModified = null,
+      EntityTagHeaderValue? entityTag = null,
+      bool enableRangeProcessing = false);

// This needs to add a remark that the byte buffer
// should not use a pooled source since it's lifetime
// isn't tied to the action result execution.
// Accessing FileContentResult.FileContents should copy the results from the memory
+ public FileContentResult File(
+     ReadOnlyMemory<byte> fileContents, 
+     string? contentType = null,
+     string? fileDownloadName = null,
+     bool enableRangeProcessing = false,
+     DateTimeOffset? lastModified = null,
+     EntityTagHeaderValue? entityTag = null);

+ public class PushFileStreamResult : FileResult
+ {
+    public Func<Stream, Task> StreamWriterCallback { get; set; }
+ }
}


We should also add PipeReader / PipeWriter overloads for controllers. @rafikiassumani-msft could we have someone propose the API for this and bring it to review?

@pranavkm pranavkm added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews labels Jan 10, 2022
@davidfowl
Copy link
Member Author

I just tried to implement this and the overloads conflicts because of how we use optional parameters... We might need to rename these or find a clever workaround.

@davidfowl
Copy link
Member Author

Actually this is fine, it's just the API analyzer complaining.

@davidfowl
Copy link
Member Author

davidfowl commented Jan 22, 2022

OK having implemented, this I think we need to support an overload of the callbacks that support range processing:

public class Results
{

// Range processing overloads
+ public IResult Stream(
+     Func<Stream, long?, long, Task> streamWriterCallback, 
+     string? contentType = null,
+     string? fileDownloadName = null,
+     DateTimeOffset? lastModified = null,
+     EntityTagHeaderValue? entityTag = null);

+ public IResult Stream(
+     Func<PipeWriter, long?, long, Task> pipeWriterCallback, 
+     string? contentType = null,
+     string? fileDownloadName = null,
+     DateTimeOffset? lastModified = null,
+     EntityTagHeaderValue? entityTag = null);

// Non-Range processing overloads
+ public IResult Stream(
+     Func<Stream, Task> streamWriterCallback, 
+     string? contentType = null,
+     string? fileDownloadName = null,
+     DateTimeOffset? lastModified = null,
+     EntityTagHeaderValue? entityTag = null);

+ public IResult Stream(
+     Func<PipeWriter, Task> pipeWriterCallback, 
+     string? contentType = null,
+     string? fileDownloadName = null,
+     DateTimeOffset? lastModified = null,
+     EntityTagHeaderValue? entityTag = null);

}

@davidfowl
Copy link
Member Author

@pranavkm brought up the fact that we might want a custom delegate type so we can document the arguments of the range processing overloads. The downside is that we need 2 types (one for pipelines and one for streams): Here's a strawman:

public delegate Task WriteRangeStreamCallback(Stream stream, long? start, long length);
public delegate Task WriteRangePipeWriterCallback(PipeWriter pipeWriter, long? start, long length); 

I'm not yet convinced...

@halter73 halter73 added api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews and removed api-approved API was approved in API review, it can be implemented labels Jan 24, 2022
@ghost
Copy link

ghost commented Jan 24, 2022

Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:

  • The PR contains changes to the reference-assembly that describe the API change. Or, you have included a snippet of reference-assembly-style code that illustrates the API change.
  • The PR describes the impact to users, both positive (useful new APIs) and negative (breaking changes).
  • Someone is assigned to "champion" this change in the meeting, and they understand the impact and design of the change.

@davidfowl
Copy link
Member Author

davidfowl commented Jan 25, 2022

We made a couple of decisions:

  • We didn't add the range overload support for these callback based APIs. That could be implemented manually. I'll make another API proposal for this.
  • We decided to remove the Func<PipeWriter, Task> overload to avoid APIs ambiguity with the Stream APIs.

@davidfowl davidfowl added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews labels Feb 5, 2022
@davidfowl davidfowl self-assigned this Feb 23, 2022
@rafikiassumani-msft rafikiassumani-msft added the help wanted Up for grabs. We would accept a PR to help resolve this issue label Jun 14, 2022
@rafikiassumani-msft
Copy link
Contributor

Triage: We will keep this issue open to give us time to add these results to Web APIs in MVC.

@0xced
Copy link
Contributor

0xced commented Sep 21, 2022

It looks like the new File method that returns a PushFileStreamResult proposed above by @pranavkm has not yet been implemented. #39383 (comment)

Can we expect it for .NET 7 ? If not, can we expect it for .NET 8 ?

@Swimburger
Copy link

Triage: We will keep this issue open to give us time to add these results to Web APIs in MVC.

Results.Stream is great. Glad to see this is on y'all's radar for MVC, as I looked for it today and didn't find the equivalent.

@amcasey amcasey added the area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc label Jun 2, 2023
@0xced
Copy link
Contributor

0xced commented Sep 2, 2023

Since it has both api-approved and help wanted tags I'd like to implement the MVC part. But before I dive into the implementation I'd like to be sure about the exact API.

@davidfowl you said

Actually this is fine, it's just the API analyzer complaining.

So is it OK to ignore RS0027?

Symbol 'File' violates the backcompat requirement: Public API with optional parameter(s) should have the most parameters amongst its public overloads.

Wouldn't it be wiser to go the same route as all the other File methods? I.e.

File(Func<Stream, Task> streamWriterCallback, string contentType);
File(Func<Stream, Task> streamWriterCallback, string contentType, bool enableRangeProcessing);
File(Func<Stream, Task> streamWriterCallback, string contentType, string? fileDownloadName);
File(Func<Stream, Task> streamWriterCallback, string contentType, string? fileDownloadName, bool enableRangeProcessing);
File(Func<Stream, Task> streamWriterCallback, string contentType, string? fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag);
File(Func<Stream, Task> streamWriterCallback, string contentType, string? fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing);
File(Func<Stream, Task> streamWriterCallback, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag);
File(Func<Stream, Task> streamWriterCallback, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing);

@0xced
Copy link
Contributor

0xced commented Sep 2, 2023

Also, I think the StreamWriterCallback property should probably be Func<Stream, CancellationToken, Task> instead of Func<Stream, Task> (i.e. with a CancellationToken parameter).

Edit: I guess one could capture the CancellationToken provided by the controller method in the callback so that's probably not necessary.

@0xced
Copy link
Contributor

0xced commented Sep 13, 2023

Well, .NET 8 RC1 was released yesterday so I guess this will be postponed to at least .NET 9 or is there still a chance to get it merged for the .NET 8 general availability (GA) release?

@mkArtakMSFT mkArtakMSFT added help candidate Indicates that the issues may be a good fit for community to help with. Requires work from eng. team and removed help wanted Up for grabs. We would accept a PR to help resolve this issue labels Oct 28, 2023
@0xced
Copy link
Contributor

0xced commented Nov 1, 2023

I see that the help wanted tag has been replaced by the help candidate tag. What does that mean for an external (non-Microsoft) contributor like me?

I'd be willing to help but I'd prefer to get answers to my questions first before diving into the implementation.

@martincostello
Copy link
Member

Details about the new process can be found here.

0xced added a commit to 0xced/aspnetcore that referenced this issue Nov 1, 2023
0xced added a commit to 0xced/aspnetcore that referenced this issue Nov 9, 2023
@captainsafia captainsafia modified the milestones: .NET 8 Planning, Backlog Mar 1, 2024
@0xced
Copy link
Contributor

0xced commented Mar 16, 2024

Thanks @martincostello for pointing out the right document. So if I understand correctly, it's up to @davidfowl (the engineer assigned to this issue) to give directions on how to move forward, right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-approved API was approved in API review, it can be implemented area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-minimal-actions Controller-like actions for endpoint routing help candidate Indicates that the issues may be a good fit for community to help with. Requires work from eng. team
Projects
None yet
Development

No branches or pull requests

10 participants