Method Definitions
All API frameworks share a common base provided by the Reflection
and Conversion
modules which
define some of the capabilities that can be used when defining an API method
signature.
Injection
This section describes how parameters of your methods are initialized and which kind of types can be used.
Primitives
By default, the following types can be used as parameters within a method definition:
string
, bool
, enum
, Guid
, DateOnly
and any other primitive type (such as
int
).
[ResourceMethod]
public int Length(string text) => text.Length;
.Get((string text) => text.Length)
public int Index(string text) => text.Length;
Parameters can be declared nullable (e.g. int?
) and will be initialized with null
if not present.
If not declared nullable, parameters will be initialized with default(T)
if not present.
By default, parameters are read from the request query (?text=abc
) or from a form encoded body.
If you would like to read the parameter directly from the request body, you can mark it with the [FromBody]
attribute.
[ResourceMethod(RequestMethod.PUT)]
public int Length([FromBody] string text) => text.Length;
.Put(([FromBody] string text) => text.Length)
[ControllerAction(RequestMethod.PUT)]
public int Index([FromBody] string text) => text.Length;
To read the parameter from the request path, use the appropriate method provided by the application framework.
[ResourceMethod(RequestMethod.DELETE, ":id")]
public void Delete(int id) { /* ... */ }
.Delete("/:id", (int id) => { /* ... */ })
[ControllerAction(RequestMethod.DELETE)]
public int Delete([FromPath] int id) { /* ... */ }
Some frameworks allow to further restrict path parameters using a regular expression.
[ResourceMethod("(?<ean13>[0-9]{12,13})")]
public Book? GetBook(int ean13) { /* ... */ }
.Get("/books/?<ean13>[0-9]{12,13})", (int ean13) => { /* ... */ })
Complex Types
When using a complex type in a parameter declaration, the value will be deserialized from the
request body. By default, handlers will accept content declared as XML, JSON, YAML or form encoded. If
the client does not declare the Content-Type
, the server will try to treat the body as JSON.
[ResourceMethod(RequestMethod.POST)]
public void Save(MyClass data) { /* ... */ }
.Post((MyClass data) => { /* ... */ })
[ControllerAction(RequestMethod.POST)]
public void Save(MyClass data) { /* ... */ }
HTML Forms
Form data can be used to populate both complex types and primitive parameters. Browsers
will encode the content as application/x-www-form-urlencoded
and the framework will
populate the arguments as needed. This allows such endpoints to be used both from
browsers and as a regular API.
Example form:
<form action="/save" method="post">
<label for="id">ID:</label>
<input type="number" id="id" name="id" required>
<br><br>
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
<br><br>
<input type="submit" value="Submit">
</form>
Can be read using the following definitions:
[ResourceMethod(RequestMethod.POST)]
public void Save(int id, string name) { /* ... */ }
// or
[ResourceMethod(RequestMethod.POST)]
public void Save(MyRecord record) { /* ... */ }
.Post("/save", (int id, string name) => { /* ... */ })
// or
.Post("/save", (MyRecord data) => { /* ... */ })
[ControllerAction(RequestMethod.POST)]
public void Save(int id, string name) { /* ... */ }
// or
[ControllerAction(RequestMethod.POST)]
public void Save(MyRecord data) { /* ... */ }
Both mechanisms can also be mixed (read one argument as a parameter and all others as a custom type).
Request Injection
To access information about the currently executed request you can add a parameter of
type IRequest
to your method definition which will automatically be populated.
[ResourceMethod]
public string? GetUserAgent(IRequest request) => request.UserAgent;
.Get("/user-agent", (IRequest request) => request.UserAgent)
public string? UserAgent(IRequest request) => request.UserAgent;
Injecting requests is required if you would like to generate custom responses.
If you frequently access the request in your endpoints to achieve a certain functionality, think about adding custom primitives, custom injectors or a custom concern.
Handler Injection
Similar to request injection you can also inject the IHandler
which is responsible for the
current request. This is typically not required but can be used to create and return
custom requests handlers on the fly.
Streams
The request body can be injected as a Stream
, e.g. when implementing
file uploads. This stream represents the processed request payload, so it
will already be decompressed and not in a chunked format. Depending on the
size of request body this will either be a stream backed by memory or by a file
and is therefore well suited for very large payloads.
[ResourceMethod(RequestMethod.PUT, "upload")]
public void Upload(Stream file) { /* ... */ }
.Put("/upload", (Stream file) => { /* ... */ })
[ControllerAction(RequestMethod.PUT)]
public void Upload(Stream file) { /* ... */ }
Custom Injection
To inject custom types besides the built-in capabilities, you can configure
a custom InjectionRegistry
and use it with your API services. The registry accepts
IParameterInjector
instances that define what types are supported and how
they are determined from the current request and environment.
The following injector will inspect the requests headers for a correlation ID and create a new one if not present.
public record class CorrelationID(string ID);
public class CorrelationInjector : IParameterInjector
{
public bool Supports(Type type) => type == typeof(CorrelationID);
public object? GetValue(IHandler handler, IRequest request, Type targetType)
{
if (request.Headers.TryGetValue("X-Correlation-ID", out var id))
{
return new CorrelationID(id);
}
return new CorrelationID(Guid.NewGuid().ToString());
}
}
The injector can than be added to the default injection registry:
var injection = Injection.Default()
.Add(new CorrelationInjector());
var api = Layout.Create()
.AddService<MyService>("service", injection);
public class MyService
{
[ResourceMethod]
public string GetCorrelationID(CorrelationID cor) => cor.ID;
}
var injection = Injection.Default()
.Add(new CorrelationInjector());
var api = Inline.Create()
.Injectors(injection)
.Get((CorrelationID cor) => cor.ID);
var injection = Injection.Default()
.Add(new CorrelationInjector());
var api = Layout.Create()
.AddController<MyController>("controller", injection);
public class MyController
{
public string GetCorrelationID(CorrelationID cor) => cor.ID;
}
User Injection
To inject the authenticated user, you can add a typed injector to your injection registry.
var injection = Injection.Default()
.Add(new UserInjector<BasicAuthenticationUser>());
Response Generation
This section describes the various mechanisms to generate a service response.
Primitives
By default, the following types can be used as a return type within a method definition:
string
, bool
, enum
, Guid
, DateOnly
and any other primitive type (such as
int
).
If declared nullable, the server will generate a HTTP 204 No Content
if null
is returned.
[ResourceMethod]
public int Length(string text) => text.Length;
.Get((string text) => text.Length)
public int Index(string text) => text.Length;
Complex Types
When returning a complex type, the value will be serialized and sent
to the client. The response format is negated with the client using the Accept
request
header. By default, the server is capable of generating XML, JSON, YAML or form encoded responses. If
no format is specified by the client, the implementation will fall back to JSON.
If declared nullable, the server will generate a HTTP 204 No Content
if null
is returned.
[ResourceMethod]
public MyType DoWork() => new();
.Get(() => new MyType())
public MyType Index() => new();
Custom Responses
When injecting the request into your method, you can directly generate an IResponse
or IResponseBuilder
and return it to the client. This allows you to take full control
over the response generation but is less readable than the typed versions.
[ResourceMethod]
public IResponseBuilder Respond(IRequest request)
{
var content = Resource.FromString("Hello World")
.Build();
return request.Respond()
.Header("X-My-Header", "my-value")
.Content(content)
.Type(ContentType.TextPlain);
}
.Get((IRequest request) => {
var content = Resource.FromString("Hello World")
.Build();
return request.Respond()
.Header("X-My-Header", "my-value")
.Content(content)
.Type(ContentType.TextPlain);
})
public IResponseBuilder Respond(IRequest request)
{
var content = Resource.FromString("Hello World")
.Build();
return request.Respond()
.Header("X-My-Header", "my-value")
.Content(content)
.Type(ContentType.TextPlain);
}
// ToDo: Doku zu möglichen Contents (verlinken bei custom handler?)
Results
Results are type-safe responses that can still be adjusted to modify the generated HTTP response. Therefore, results can be considered an advanced way to generate responses without the need to fully generate the response in the first place.
[ResourceMethod]
public Result<string> DoWork()
{
return new Result<string>("Hello World").Header("X-My-Header", "my-value");
}
.Get(() => new Result<string>("Hello World").Header("X-My-Header", "my-value"))
public Result<string> Index()
{
return new Result<string>("Hello World").Header("X-My-Header", "my-value");
}
The Result<T>
class allows to adjust any response property besides the actual content
by implementing the same interfaces as the IResponseBuilder
. This does not only work
for data structures, but also for special types such as streams.
Handlers
Instead of generating a response you can also return an IHandler
or IHandlerBuilder
instance.
This allows you to provide a whole segment on your web application by re-using the
existing handlers or by implementing custom ones.
The following example will render a fully navigable directory listing view depending on the tenant ID passed to the method:
[ResourceMethod("files/:tenant")]
public IHandlerBuilder Files(int tenant)
{
var tree = ResourceTree.FromDirectory($"/data/tenants/{tenant}");
return Listing.From(tree);
}
.Get("/files/:tenant", (int tenant) => Listing.From(ResourceTree.FromDirectory($"/data/tenants/{tenant}")))
public IHandlerBuilder Files(int tenant)
{
var tree = ResourceTree.FromDirectory($"/data/tenants/{tenant}");
return Listing.From(tree);
}
Streams
To return files or similar content, you can directly return a Stream
instance
from your method.
The framework will automatically seek and dispose the stream. Returning streams
is not thread-safe as streams are stateful, so you will need to create a new
instance for every request to be answered.
[ResourceMethod]
public Stream GetFile() => File.OpenRead("...");
.Get(() => File.OpenRead("..."))
public Stream File() => File.OpenRead("...");
Empty Responses
Methods with a void
return type will automatically generate a HTTP 204 No Content
response. This is also the case when null
is returned.
[ResourceMethod]
public void DoWork() { }
.Get(() => { }})
public void DoWork() { }
Asynchronous Execution
Service methods returning a Task
or ValueTask
will be executed
asynchronously. All the features described in this document will work
for asynchronous execution as well.
[ResourceMethod]
public async ValueTask<int> DoWork() { /* ... */ }
.Get(async () => await ...)
public async ValueTask<int> DoWork() { /* ... */ }
Registries
This section describes registries that can be used for both injection as well as response generation.
Custom Primitives
Primitives (such as Guid
or int
) used in parameters or as a response type are automatically
handled using the built-in FormatterRegistry
. You can add support for a custom type by
implementing an IFormatter
and adding it to a custom registry which is then used by your
services.
The following implementation will add support for a Point
type with x
and y
coordinates
so it can be used in a service.
public record class Point(int X, int Y);
public class PointFormatter : IFormatter
{
public bool CanHandle(Type type) => type == typeof(Point);
public object? Read(string value, Type type)
{
var parts = value.Split('-');
return new Point(int.Parse(parts[0]), int.Parse(parts[1]));
}
public string? Write(object value, Type type)
{
var point = (Point)value;
return $"{point.X}-{point.Y}";
}
}
This formatter can then be added to the default formatting registry.
var registry = Formatting.Default()
.Add(new PointFormatter());
var api = Layout.Create()
.AddService<MyService>("service", registry);
public class MyService
{
[ResourceMethod("invert/:point")]
public Point Invert(Point point) => new(point.Y, point.X);
}
var registry = Formatting.Default()
.Add(new PointFormatter());
var api = Inline.Create()
.Formatters(registry)
.Get("/invert/:point", (Point point) => new(point.Y, point.X));
var registry = Formatting.Default()
.Add(new PointFormatter());
var api = Layout.Create()
.AddController<MyController>("controller", registry);
public class MyController
{
public Point Invert([FromPath] Point point) => new(point.Y, point.X);
}
The serialization format is implemented by our formatter, so the API can be called
via /invert/8-10
and will return 10-8
as a text formatted response.
Serialization Formats
Serialization allows to read and write complex types, so they can be used in your method definitions. By default, services can consume and produce entities in XML, JSON, YAML or in form encoding, with a default fallback to JSON.
When sending an entity to your service, the client should specify the Content-Type
of the body so the server can choose the correct deserializer to read the data with. The Accept
header sent by the client tells the server which serialization format is preferred by
the client when a response is generated. The server will tell the client which
serialization format was used to generate the body of the response by specifying the
Content-Type
header again.
To add support for an additional format you can implement the ISerializationFormat
and
add your implementation to a registry which is then passed to your service.
For example, the nuget package GenHTTP.Modules.Protobuf
adds support for
Protocol Buffers which is not enabled by default. The
following snippet shows how to register the protobuf format and use it in
a service.
var registry = Serialization.Default()
.Add(new FlexibleContentType("application/protobuf"), new ProtobufFormat());
var api = Layout.Create()
.AddService<MyService>("service", registry);
public class MyService
{
[ResourceMethod(RequestMethod.PUT)]
public ResponseType Store(RequestType data) { /* ... */ }
}
var registry = Serialization.Default()
.Add(new FlexibleContentType("application/protobuf"), new ProtobufFormat());
var api = Inline.Create()
.Serializers(registry)
.Put((RequestType data) => { /* ... */ });
var registry = Serialization.Default()
.Add(new FlexibleContentType("application/protobuf"), new ProtobufFormat());
var api = Layout.Create()
.AddController<MyController>("controller", registry);
public class MyController
{
[ControllerAction(RequestMethod.PUT)]
public ResponseType Store(RequestType data) { /* ... */ }
}