Returning ZIP Files and Hypermedia in Minimal APIs

While in Minimal APIs built-in results like Results.Ok() cover most use cases, sometimes you need to return custom content types such as ZIP files or Hypermedia (HATEOAS) responses. In these scenarios, implementing a custom IResult gives you full control over headers, content type, and response serialization.

Understanding IResult

The interface IResult is the core abstraction for all Minimal API possible responses. Any class implementing IResult can be returned from a Minimal API endpoint. The interface has a very basic and simple structure:

public interface IResult
{
Task ExecuteAsync(HttpContext httpContext);
}

The ExecuteAsync method is called by the runtime to write headers and the response body. This makes IResult ideal for streaming content, returning multipart data, or producing non-standard formats.

In particular, the helper method Results.Ok returns an IResult representing an HTTP 200 response with optional content. Its purpose is to make endpoint code concise while letting the framework handle serialization, status code, and headers. Here’s some pseudo-code that gives an idea of its internal implementation and, at the same time, lays the ground for custom implementations.

public async Task ExecuteAsync(HttpContext httpContext)
{
    httpContext.Response.StatusCode = 200;
    httpContext.Response.ContentType = "application/json";
    await httpContext.Response.WriteAsJsonAsync( obj );
}

As long as you intend to return a JSON serializable object, you can safely and effectively use Results.Ok; if you need the Minimal API endpoint to return a special flavor of data (i.e., a zipped file) then you may need to create a custom result type.

Returning a ZIP File

Suppose you want to return a dynamically generated ZIP file containing multiple documents. A custom IResult allows you to stream the ZIP directly to the client without creating temporary files:

public class ZipResult : IResult
{
private readonly byte[] _content;
private readonly string _filename;
public ZipResult(byte[] content, string filename)
{
_content = content;
_filename = filename;
}
public async Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = "application/zip";
httpContext.Response.Headers["Content-Disposition"] = $"attachment; filename={_filename}";
await httpContext.Response.Body.WriteAsync(_content,
0, _content.Length);
}
}

Then, your endpoint can return the ZIP result:

app.MapGet("/export", () =>
{
byte[] zipBytes = GenerateZip();
return new ZipResult(zipBytes, "archive.zip");
});

The method GenerateZip is a placeholder for any logic that returns the array of bytes with zipped content. Creating a ZIP archive in C# is straightforward thanks to the built-in System.IO.Compression namespace. You have a few options depending on whether you want to create a ZIP from files on disk or build one dynamically in memory. Here’s how to zip a few disk files in memory:

using System.IO;
using System.IO.Compression;
byte[] CreateZip()
{
using var memoryStream = new MemoryStream();
using (var zip = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) {
// Add first file
var entry = zip.CreateEntry("file1.txt");
using var entryStream = entry.Open();
using var writer = new StreamWriter(entryStream);
writer.Write("Hello World!");// Add another file
var entry2 = zip.CreateEntry("file2.txt");
using var entry2Stream = entry2.Open();
using var writer2 = new StreamWriter(entry2Stream);
writer2.Write("Another file content");
return memoryStream.ToArray(); // Return ZIP as byte array
}

To be honest, once you have zipped content, which you can create through the ZipArchive .NET class, you don’t strictly need a custom ZipResult class to pack it all. It definitely is a convenience, but not a necessity. You can return a ZIP archive directly from a Minimal API by streaming bytes with Results.File which ASP.NET Core provides exactly for this use case.

Results.File(byte[], contentType, fileDownloadName)

It is quite different, instead, if you intend to return something that doesn’t natively exist in the .NET Core framework and that may not naturally fit in a raw text or byte array. For example, hypermedia content.

Returning Hypermedia Responses

For Hypermedia-driven APIs (HATEOAS), you may want to include links alongside the data. You can create a custom IResult that serializes both the payload and associated links:

public class HypermediaResult<T> : IResult
{
private readonly T _data;
private readonly object _links;
public HypermediaResult(T data, object links)
{
_data = data;
_links = links;
}
public async Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = "application/json";
var payload = new { data = _data, _links = _links };
await httpContext.Response.WriteAsJsonAsync(payload);
}
}

Here’s an example endpoint using the new custom result type.

app.MapGet("/user/{id}", (int id) =>
{
var user = GetUser(id);
if (user == null) return Results.NotFound();
var links = new {
self = $"/user/{id}",
orders = $"/user/{id}/orders"
};
return new HypermediaResult(user, links);
});

Overall, using a custom IResult type has some benefits. First and foremost, you gain full control over HTTP headers and content type. Next, you work naturally with Minimal API’s type inference and routing and can effectively stream large or multipart content if needed. With a specific reference to HATEOAS, namely Hypermedia As The Engine Of Application State, a custom IResult type gives total control over the links to include in the response, their related actions and the programming interface to group them together.

Conclusion

Custom IResult types in Minimal APIs are powerful tools for scenarios where built-in results are insufficient. Whether returning ZIP files, PDFs, or Hypermedia-enriched JSON, implementing IResult allows you to control serialization, headers, and streaming efficiently while keeping endpoints clean and focused. This pattern complements existing helpers like Results.Ok and Results.Json, providing a fully extensible foundation for advanced API scenarios.

Published by D. Esposito

Software person since 1992

Leave a comment