Coming from GenHTTP
If you know GenHTTP, you already know genhttp-kt. The API is the same - the same builders, modules, handlers and concerns - written the way a Kotlin developer expects. This page shows how C# translates, so you can use the samples you find for GenHTTP, and the GenHTTP documentation itself, with genhttp-kt.
The samples on this page have a C# and a Kotlin tab. Choose one and every sample of the page switches with it.
Packages
Every NuGet package of GenHTTP is a Maven artifact of genhttp-kt with the group org.genhttp.
Module names are written in kebab-case for the artifact and in lowercase for the package:
| GenHTTP (NuGet) | genhttp-kt (Maven) | Package |
|---|---|---|
GenHTTP.Core | org.genhttp:genhttp-core | org.genhttp.engine.internal (the engine) |
GenHTTP.Full | org.genhttp:genhttp-full | - |
GenHTTP.Core.Ioxide | org.genhttp:genhttp-core-netty | org.genhttp.engine.netty (the Netty engine) |
GenHTTP.Full.Ioxide | org.genhttp:genhttp-full-netty | - |
GenHTTP.Api | org.genhttp:genhttp-api | org.genhttp.api |
GenHTTP.Modules.Functional | org.genhttp:genhttp-modules-functional | org.genhttp.modules.functional |
GenHTTP.Modules.SinglePageApplications | org.genhttp:genhttp-modules-single-page-applications | org.genhttp.modules.singlepageapplications |
GenHTTP.Testing | org.genhttp:genhttp-testing | org.genhttp.testing |
A using GenHTTP.Modules.Functional; becomes import org.genhttp.modules.functional.*. Many
features of the modules are extension functions, as they are extension methods in C#, so import
the package of a module with a wildcard - without the import, the compiler only sees part of
the overloads and reports a type mismatch.
Naming
| C# | Kotlin |
|---|---|
IHandler, IRequest, IResponseBuilder | Handler, Request, ResponseBuilder - no I prefix |
Respond(), Header, Content(...) | respond(), header, content(...) - camelCase |
HandleAsync() returning ValueTask | suspend fun handle() - no Async suffix |
[ResourceMethod] (ResourceMethodAttribute) | @ResourceMethod - no Attribute suffix |
ContentType.TextHtml, ResponseStatus.Ok | ContentType.TextHtml, ResponseStatus.Ok - unchanged |
Resource.FromString("...") (static factory) | Resource.fromString("...") (companion extension) |
Layout.Create().Add("api", api) | Layout.create().add("api", api) |
Builders are kept as they are: a factory returns a builder, every builder function returns the
builder, and build() creates the instance. You may still use Kotlin’s apply { } on any of
them.
Types
| C# / .NET | Kotlin / JVM |
|---|---|
ValueTask, Task, ValueTask<T> | suspend fun ..., suspend fun ...: T |
Func<A, R>, Action<A> | (A) -> R, (A) -> Unit |
Action<IResponseBuilder> | ResponseBuilder.() -> Unit (a lambda with receiver) |
Stream | InputStream (reading), OutputStream (writing) |
ReadOnlyMemory<byte>, byte[] | ByteBuffer (read-only views), ByteArray |
DateTime, TimeSpan | java.time.Instant, kotlin.time.Duration |
Uri, IPAddress, CultureInfo, Guid | URI, InetAddress, Locale, UUID |
FileInfo, DirectoryInfo | java.nio.file.Path |
X509Certificate2 (with private key) | KeyStore.PrivateKeyEntry |
ILogger, ILoggerFactory | SLF4J Logger, ILoggerFactory |
record | data class |
IDisposable, IAsyncDisposable | AutoCloseable, suspend fun close() |
TryGet(key, out value) | tryGet(key) returning a nullable value |
params T[], IEnumerable<T> | vararg, Iterable<T> |
Embedded resources of an Assembly | The classpath (Resource.fromClasspath(...)) |
Libraries
Where GenHTTP uses a library of .NET, genhttp-kt uses the established one of the JVM:
| Purpose | GenHTTP | genhttp-kt |
|---|---|---|
| JSON, XML, YAML | System.Text.Json, XmlSerializer, YamlDotNet | Jackson 3 with its Kotlin module |
| Protobuf | protobuf-net | kotlinx-serialization-protobuf |
| Templates | Cottle | Pebble |
| Open API | NSwag | swagger-core |
| JWT | System.IdentityModel | Nimbus JOSE + JWT |
| Archives | SharpCompress | Apache Commons Compress |
| Brotli, Zstandard | .NET | Brotli4j, zstd-jni |
| Dependency injection | Microsoft.Extensions.DependencyInjection | Koin |
| HTTP client (proxies) | HttpClient | java.net.http.HttpClient |
| Logging | Microsoft.Extensions.Logging | SLF4J |
Side by Side
Hosting
A server is created, configured and run exactly as in GenHTTP. RunAsync() becomes the
suspending run(), which blocks the coroutine until the application is shut down.
Kotlin
import org.genhttp.api.content.io.Resource
import org.genhttp.engine.internal.Host
import org.genhttp.modules.io.*
import org.genhttp.modules.practices.*
suspend fun main() {
val content = Content.from(Resource.fromString("Hello World!"))
Host.create()
.handler(content)
.defaults()
.run()
}C#
using GenHTTP.Engine.Internal;
using GenHTTP.Modules.IO;
using GenHTTP.Modules.Practices;
var content = Content.From(Resource.FromString("Hello World!"));
await Host.Create()
.Handler(content)
.Defaults()
.RunAsync();Functional Handlers
Lambdas become suspend lambdas with typed parameters - their names and types are used to map
the request, as in C#. Kotlin does not allow annotations or default values on lambda parameters,
so an operation that needs them ([FromBody], int page = 1) is a function reference instead.
Kotlin
import org.genhttp.modules.functional.*
import org.genhttp.modules.reflection.FromBody
fun echo(@FromBody body: String) = body
fun createApi() = Inline.create()
.get("/greet/:name") { name: String -> "Hello $name!" }
.post("/echo", ::echo)C#
using GenHTTP.Modules.Functional;
using GenHTTP.Modules.Reflection;
var api = Inline.Create()
.Get("/greet/:name", (string name) => $"Hello {name}!")
.Post("/echo", ([FromBody] string body) => body);Webservices
Attributes become annotations, Task<T> becomes a suspend function, records become data
classes.
Kotlin
import kotlinx.coroutines.delay
import org.genhttp.modules.layouting.*
import org.genhttp.modules.reflection.FromBody
import org.genhttp.modules.reflection.Method
import org.genhttp.modules.webservices.*
data class Greeting(val text: String)
class GreetingResource {
@ResourceMethod(path = ":name")
fun greet(name: String) = Greeting("Hello $name!")
@ResourceMethod(Method.Post)
suspend fun echo(@FromBody body: String): String {
delay(10)
return body
}
}
fun createApp() = Layout.create()
.addService<GreetingResource>("greetings")C#
using GenHTTP.Modules.Layouting;
using GenHTTP.Modules.Reflection;
using GenHTTP.Modules.Webservices;
var app = Layout.Create()
.AddService<GreetingResource>("greetings");
public record Greeting(string Text);
public class GreetingResource
{
[ResourceMethod(":name")]
public Greeting Greet(string name) => new($"Hello {name}!");
[ResourceMethod(RequestMethod.Post)]
public async Task<string> Echo([FromBody] string body)
{
await Task.Delay(10);
return body;
}
}Custom Handlers
IHandler becomes Handler, its methods are suspend functions. On the internal engine, a
handler may block - every request runs on a virtual thread - and may suspend as well. On the
Netty engine, handlers run on the event loop and must suspend
instead of blocking, as on ioxide.
Kotlin
import org.genhttp.api.content.Handler
import org.genhttp.api.infrastructure.Server
import org.genhttp.api.protocol.ContentType
import org.genhttp.api.protocol.Request
import org.genhttp.api.protocol.Response
import org.genhttp.modules.io.*
class HelloHandler : Handler {
override suspend fun prepare(server: Server) {}
override suspend fun handle(request: Request): Response? =
request.respond()
.content("Hello World!", ContentType.TextPlain)
.build()
}C#
using GenHTTP.Api.Content;
using GenHTTP.Api.Infrastructure;
using GenHTTP.Api.Protocol;
using GenHTTP.Modules.IO;
public class HelloHandler : IHandler
{
public ValueTask PrepareAsync(IServer server) => ValueTask.CompletedTask;
public ValueTask<IResponse?> HandleAsync(IRequest request)
{
var response = request.Respond()
.Content("Hello World!", ContentType.TextPlain)
.Build();
return new(response);
}
}Resources
Embedded resources of an assembly are resources on the classpath - in a Gradle project, the
files below src/main/resources. A classpath folder is found in every root of the classpath,
in the IDE as well as in a packaged JAR.
Kotlin
import org.genhttp.api.content.io.Resource
import org.genhttp.api.content.io.ResourceTree
import org.genhttp.modules.files.*
import org.genhttp.modules.io.*
import org.genhttp.modules.layouting.*
fun createApp() = Layout.create()
.index(Content.from(Resource.fromClasspath("web/index.html")))
.add("static", Assets.from(ResourceTree.fromClasspath("web/static")))C#
using GenHTTP.Modules.Files;
using GenHTTP.Modules.IO;
using GenHTTP.Modules.Layouting;
var app = Layout.Create()
.Index(Content.From(Resource.FromAssembly("index.html")))
.Add("static", Assets.From(ResourceTree.FromAssembly("static")));What is Not There
A few parts of GenHTTP are specific to .NET and have no counterpart in genhttp-kt:
- The Kestrel engine - genhttp-kt runs on its own engines: the port of GenHTTP’s internal engine and the Netty engine, which takes the place of Ioxide.
- The ASP.NET Core adapter, as ASP.NET Core does not exist on the JVM.
- Roslyn code generation for web services - genhttp-kt calls operations through method handles and kotlin-reflect, see Execution Modes.
Everything else is there. Where genhttp-kt intentionally behaves differently, the deviation log says so and explains why.