This library contains some ideas and implementations meant to eliminate boilerplate code from commonly used functionalities. Currently the different examples contains: Hierarchy aware polymorphic JSON serialization and Convention based message handlers and factories. Whose usages are intertwined and complement each other.
The Pinja.Examples.Extensibility
library contains a library for implementing type hierarchy aware, version tolerant, and extensible polymorphic serialization implemented with System.Text.Json
converters.
The basic idea behind this library is that you could inheritance when defining your message types so that assign more abstract semantics to the types and members closed to the root type and more concrete semantics to types an their members at the leaf level of the inheritance hierarchy. Of course this all means nothing if you can't transport the data around and handle it where ever you need at the level of abstraction that you need.
The problem commonly with this type of approach is that data tends to be lost or the handling is downright impossible if the receiving end is at some level unfamiliar with some part of the message.
.NET has the IExtensibleDataObject
interface used in data contract serializers to store properties that are not recognized to be a part of the data contract and that all works well until it encounters a contract that it does not recognize and then all breaks down.
With data contract serializes the contract only has information about the specific contract and nothing about the hierarchy. This means that when you introduce a new derived type to a hierarchy you must update all clients immediately to use the new version of the library or else
the serialization will fail even if some clients would not need to use any part of the new derived type.
In this library the metadata about the contract contains the metadata about the hierarchy. This way, if a client is unfamiliar with the specific contract, it can move towards the root of the hierarchy until in encounters a contract that it recognizes and then it can deserialize the message with the semantics of that level of abstraction. The properties or members that are not recognized as well as the exact hierarchy metadata is also preserved so that if the client needs to serialize the message again no data is lost.
This approach has the benefit that if some client only works on an abstract level with the messages passed around in a system it is not broken if publishers of messages publish messages of derived types. This makes deployments in systems with lots of different services easier since all of the services don't necessarily need to be deployed at once and in a very specific order.
Of course adding the hierarchy information as a part of the serialized message adds to the payload size but that is the price to pay in this approach.
For more information I invite you to read the code and try in out for yourself.
The Pinja.Examples.ConventionBasedFactories
contains a way of defining handler types for different message types in a way that aims to eliminate the boiler plate code associated with routing handling of different message types to different handler implementations.
In systems with multiple different services that need to talk to each other message passing is a good way to go about implementing service to service communication using some kind of message bus at the infrastructure level. Then it is not uncommon that new types of messages get introduced throughout the life time of the system. When adding new types of message it is also necessary to add some routing code so that the messages get where they need to go. It is fairly cumbersome to always add new message busses to the infrastructure when adding new message types so one could use a common bus to transport different kinds of messages between service. Then when the message gets delivered to a service it needs to go somewhere. Often there is some kind of discriminator in the message itself or in some metadata property that can be used to route the message to a correct handler. This can be implemented with if-else control structures, switch-case control structures, or maybe dictionaries with delegates or possibly interfaces. Often when new messages are added this causes a need to also touch the control structures that route the messages to their handlers. If there are many of these kind of structures it is very easy to forget to update some of them.
This library aims to eliminate the need to touch the routing control structures when adding new messages and handlers for them. After all the control structure lives at a different level of abstraction that a very specific message. Why should a change, addition more less, force you to change and modify existing code that is not directly related to the level of abstraction you are working on?
The approach here is to specify handlers for different types of messages in a declarative way and let existing code to do the routing based on that. The convention part here is that each message handler is abstracted behind a common and simple interface that simply provides a method to handle the message the handler is created for. The other convention behind the scenes is that each handler for a specific message hierarchy is forced to have a constructor with the same signature. This can be seen as a negative feature but if also forces you to keep the signature at a certain level of abstraction and helps keep it from bloating. If handling of different message require specific dependencies then a service provider of some sort can be use as a common dependency for all message handlers.
The example library defines game events and handlers for those. Each game event defines a EventType
property that is used as the message discriminator. This discriminator is used in the GameEventHandlerAttribute
that is applied to all game event handlers.
The GameEventHandlerFactory
is responsible of creating the handler instances for the game events. This class is very simple and does not have to be touched after the initial commit.
When new game events and their handlers are introduced they are automatically added to the factory by using the attributes.
Read the code to learn more.