Response Content
When sending content in response to a client request, IResponseBuilder has a single entry point for it:
Content(IResponseContent? content). Everything about the content itself - its Length, Type and
encoding - is a property of the IResponseContent implementation being passed in, not something configured
separately on the builder. Length is the number of bytes the content will produce; if it is not known,
chunked transfer encoding is applied.
The API provides a minimal protocol to allow the server engines to accept and consume the content to be sent to the client. It does (and should not) know about files, streams, assemblies and so on.
To bridge the gap between the API and the real world, there is the I/O module which provides typically
used types of content as well as extension methods to make it easier to work with content. Most of them
accept an optional ContentType? parameter, so you rarely need to construct an IResponseContent
implementation yourself just to set a type:
using GenHTTP.Api.Protocol;
using GenHTTP.Modules.IO;
request.Respond()
.Content("Hello World", ContentType.TextPlain);Content Type Guessing
When working with files that are not necessarily under your control, you might need to dynamically
determine the content type of a given file. For this, you can use the GuessContentType() extension
provided by the I/O module, available both on string file names and on resources
(where it additionally checks the resource’s own declared content type before falling back to guessing
from its name):
using GenHTTP.Modules.IO;
var type = "style.css".GuessContentType() ?? ContentType.ApplicationOctetStream;
var resourceType = resource.GuessContentType(); // falls back to ApplicationForceDownloadContent Implementations
The I/O module adds StringContent, StreamContent and ResourceContent which can be used
to send corresponding content to the client. Each takes the ContentType to use as a constructor
argument. The following example will show you how to send strings, streams or resources
as a response:
using GenHTTP.Api.Protocol;
using GenHTTP.Engine.Internal;
using GenHTTP.Modules.IO;
using GenHTTP.Modules.IO.Streaming;
using GenHTTP.Modules.Layouting;
using GenHTTP.Modules.Practices;
using GenHTTP.Modules.Webservices;
using StreamContent = GenHTTP.Modules.IO.Streaming.StreamContent;
using StringContent = GenHTTP.Modules.IO.Strings.StringContent;
var app = Layout.Create()
.AddService<ContentExamples>("content");
await Host.Create()
.Handler(app)
.Defaults()
.RunAsync();
class ContentExamples
{
[ResourceMethod("get-string")]
public IResponseBuilder GetString(IRequest request)
{
return request.Respond()
.Content(new StringContent("This is a string", ContentType.TextPlain));
}
[ResourceMethod("get-resource")]
public IResponseBuilder GetResource(IRequest request)
{
var resource = Resource.FromString("This is a string") // or from any other source
.Build();
return request.Respond()
.Content(new ResourceContent(resource, ContentType.TextPlain));
}
[ResourceMethod("get-stream")]
public IResponseBuilder GetStream(IRequest request)
{
var stream = new MemoryStream("This is a string"u8.ToArray());
return request.Respond()
.Content(new StreamContent(stream, ContentType.TextPlain, (ulong)stream.Length, null, stream.CalculateChecksumAsync));
}
}To simplify their usage, the module also adds extensions directly on IResponseBuilder, which
are recommended to be used if possible:
using GenHTTP.Api.Protocol;
using GenHTTP.Engine.Internal;
using GenHTTP.Modules.IO;
using GenHTTP.Modules.Layouting;
using GenHTTP.Modules.Practices;
using GenHTTP.Modules.Webservices;
var app = Layout.Create()
.AddService<ContentExamples>("content");
await Host.Create()
.Handler(app)
.Defaults()
.RunAsync();
class ContentExamples
{
[ResourceMethod("get-string")]
public IResponseBuilder GetString(IRequest request)
{
return request.Respond()
.Content("This is a string");
}
[ResourceMethod("get-resource")]
public IResponseBuilder GetResource(IRequest request)
{
var resource = Resource.FromString("This is a string") // or from any other source
.Build();
return request.Respond()
.Content(resource);
}
[ResourceMethod("get-stream")]
public IResponseBuilder GetStream(IRequest request)
{
var stream = new MemoryStream("This is a string"u8.ToArray());
return request.Respond()
.Content(stream, ContentType.TextPlain, (ulong)stream.Length, checksumProvider: stream.CalculateChecksumAsync);
}
}Custom Content Implementations
The following code shows an example on how we can implement IResponseContent
to efficiently serve the data stored in an entity record via Content(). Besides Length, an
implementation needs to expose its own Type and Encoding and write itself to the IResponseSink
passed to WriteAsync:
public class Attachment
{
public int Id { get; set; }
public long Size { get; set; }
public DateTime Modified { get; set; }
public ReadOnlyMemory<byte> Data { get; set; }
}
public class AttachmentContent(Attachment attachment) : IResponseContent
{
public ulong? Length => (ulong)attachment.Size;
public ContentType? Type => ContentType.ApplicationForceDownload;
public ReadOnlyMemory<byte>? Encoding => null;
public ValueTask<ulong?> CalculateChecksumAsync() => new((ulong)attachment.Modified.Ticks);
public ValueTask WriteAsync(IResponseSink sink)
{
sink.Writer.Write(attachment.Data.Span);
return ValueTask.CompletedTask;
}
}
public class AttachmentContentHandler : IHandler
{
public ValueTask PrepareAsync(IServer server) => ValueTask.CompletedTask;
public ValueTask<IResponse?> HandleAsync(IRequest request)
{
var id = request.Header.Query.GetEntry("id");
if (id != null)
{
// load the entity from some DB
var entity = ...
return request.Respond()
.Content(new AttachmentContent(entity))
.Build();
}
return new();
}
}
await Host.Create()
.Handler(new AttachmentContentHandler())
.Defaults()
.RunAsync();While this is more complex than simply returning a Stream from a web service,
it is way more efficient for caching as we can use the modification date of the entity
to check for changes.