Recently I got into rewritting a ASP .Net framework webservice, that grants access to various parts of a large multiple-processes-system. Not like your typical CRUD entries in database stuff, but actually remote controlling processes. Many of the typical guides one finds online do not relate in the slightest to this stuff.
So, the first thing I did was to split the service up into many. I wanted “use case pillars”, one service (plus whatever comes in micro services or processes behind it) per type of resource that can be subscribed to or requested/consumed by the client. IPC between services and processes happens with ZMQ + GPB, a combination I simply love. These pillars should not interfere with each other, have no dependencies to each other, so in one regard they scale well and independently, and in the other they can be deployed selectively. Another benefit of having ZMQ between service and our backend processes is that the services don’t need to live on the same machine, but can be anywhere - provided Curve ZMQ is applied.
Then there’s a service handling bidirectional communication with SignalR - following Armin’s thougts about websockets: to keep them far far away from any other application code. That was before gRPC became popular (and it’s still not great for browser fronts).
My template of projects looked like this:
AllMyServices.sln/
├── Common base Infrastructure/
│ ├── middlewares
│ ├── settings
│ ├── shared startup extensions
│ ├── swashbuckle setup
│ ├── filters
│ ├── shared models
│ ├── shared A&A handling
│ └── ...
├── Shared GPB definitions
├── Shared Logger Extension that sends into a common pool
├── Shared Zmq Client implementation
├── ServiceA/
│ ├── ServiceA.API: actual hosting service, containing controllers
│ ├── ServiceA.DataContracts: containing all the models
│ ├── ServiceA.GPB: containing ServiceA-specific protobuf interfaces,
│ │ automatically generated on build
│ ├── ServiceA.Mapping: Mapping of models
│ ├── ServiceA.Services: Handling async communication to backend processes,
│ │ either in a transient way or in a standing connection
│ └── ServiceA.UnitTests
├── ServiceB/
│ └── ...
├── EventService/
│ ├── EventHub: SignalR hubs for events/bidirectional communication stuff
│ │ ├── Contracts: Service interfaces
│ │ ├── PartialHubs: SignalR specifics
│ │ ├── Subscriptions: managing resource subscription
│ │ └── Services: holding standing connections to the backend
│ ├── EventService: Hosting app
│ └── EventService.UnitTests
├── Common.props: shared properties, such as target framework, lock files, ...
├── Common.references: pin down the dependencies each service shares
└── Common.targets: shared build targets
A simple pattern to follow, new services can be generated from templates and just added - so extensions can be done easily and quickly, and by anyone without much introduction to the environment. Anything that is required by any service is shared in libs to be either included or injected.
Also, if you start a new project, always include metrics tracking from the very start. Good to always keep an eye on performance, see what’s possible and easier to impress management.