IServer: converting hierarchy to composition
Published by Marco on
Quino has long included support for connecting to an application server instead of connecting directly to databases or other sources. The application server uses the same model as the client and provides modeled services (application-specific) as well as CRUD for non-modeled data interactions.
We wrote the first version of the server in 2008. Since then, it’s acquired better authentication and authorization capabilities as well as routing and state-handling. We’ve always based it on the .NET
Old and Busted
As late as Quino 2.0-beta2 (which we had deployed in production environments already), the server hierarchy looked like screenshot below, pulled from issue QNO-4927:
This screenshot was captured after a few unneeded interfaces had already been removed. As you can see by the class names, we’d struggled heroically to deal with the complexity that arises when you use inheritance rather than composition.
The state-handling was welded onto an authentication-enabled server, and the base machinery for supporting authentication was spread across three hierarchy layers. The hierarchy only hints at composition in its naming: the “Stateful” part of the class name
CoreStatefulHttpServerBase<TState> had already been moved to a state provider and a state creator in previous versions. That support is unchanged in the 2.0 version.
We mentioned above that implementation was “spread across three hierarchy layers”. There’s nothing wrong with that, in principle. In fact, it’s a good idea to encapsulate higher-level patterns in a layer that doesn’t introduce too many dependencies and to introduce dependencies in other layers. This allows applications not only to be able to use a common implementation without pulling in unwanted dependencies, but also to profit from the common tests that ensure the components works as advertised.
In Quino, the following three layers are present in many components:
- Abstract: a basic encapsulation of a pattern with almost no dependencies (generally just
- Standard: a functional implementation of the abstract pattern with dependencies on non-metadata assemblies (e.g.
Encodo.Connectionsand so on)
- Quino: an enhancement of the standard implementation that makes use of metadata to fill in implementation left abstract in the previous layer. Dependencies can include any of the Quino framework assemblies (e.g.
Quino.Applicationand so on).
The New Hotness
The diagram below shows the new hotness in Quino 2.
The hierarchy is now extremely flat. There is an
IServer interface and a
Server implementation, both generic in
TListener, of type
IServerListener. The server manages a single instance of an
The listener, in turn, has an
IHttpServerRequestHandler, the main implementation of which uses an
As mentioned above, the
IServerStateProvider is included in this diagram, but is unchanged from Quino 2.0-beta3, except that it is now used by the request handler rather than directly by the server.
You can see how the abstract layer is enhanced by an HTTP-specific layer (the
Encodo.Server.Http namespace) and the metadata-specific layer is nice encapsulated in three classes in the
Server Components and Flow
This type hierarchy has decoupled the main elements of the workflow of handling requests for a server:
- The server manages listeners (currently a single listener), created by a listener factory
- The listener, in turn, dispatches requests to the request handler
- The request handler uses the route handler to figure out where to direct the request
- The route handler uses a registry to map requests to response items
- The request handler asks the state provider for the state for the given request
- The state provider checks its cache for the state (the default support uses persistent states to cache sessions for a limited time); if not found, it creates a new one
- Finally, the request handler checks whether the user for the request is authenticated and/or authorized to execute the action and, if so, executes the response items
It is important to note that this behavior is unchanged from the previous version—it’s just that now each step is encapsulated in its own component. The components are small and easily replaced, with clear and concise interfaces.
Note also that the current implementation of the request handler is for HTTP servers only. Should the need arise, however, it would be relatively easy to abstract away the
HttpListener dependency and generalize most of the logic in the request handler for any kind of server, regardless of protocol and networking implementation. Only the request handler is affected by the HTTP dependency, though: authentication, state-provision and listener-management can all be re-used as-is.
Also of note is that the only full-fledged implementation is for metadata-based applications. At the bottom of the diagram, you can see the metadata-specific implementations for the route registry, state provider and authenticator. This is reflected in the standard registration in the IOC.
These are the service registrations from
return handler .RegisterSingle<IServerSettings, ServerSettings>() .RegisterSingle<IServerListenerFactory<HttpServerListener>, HttpServerListenerFactory>() .Register<IServer, Server<HttpServerListener>>();
And these are the service registrations from
handler .RegisterSingle<IServerRouteRegistry<IMetaServerState>, StandardMetaServerRouteRegistry>() .RegisterSingle<IServerStateProvider<IMetaServerState>, MetaPersistentServerStateProvider>() .RegisterSingle<IServerStateCreator<IMetaServerState>, MetaServerStateCreator>() .RegisterSingle<IHttpServerAuthenticator<IMetaServerState>, MetaHttpServerAuthenticator>() .RegisterSingle<IHttpServerRequestHandler, HttpServerRequestHandler<IMetaServerState>>()
As you can see, the registration is extremely fine-grained and allows very precise customization as well as easy mocking and testing.