Let me preface by saying the RabbitMQ code is pretty bad. It’s just enough to get it working. The RabbitMQ implementation of the service layer isn’t much more than POC.
Anyway: the idea is to host and consume services, all ignorantly. As a developer writing code to be hosted in the app server, you shouldn’t need to know how the services are being hosted, or how to call them. Today the app server does it using RabbitMQ. Maybe tomorrow it’s using Service Bus for Windows or Web Api. I want all of that abstracted away leaving the developer with simple interfaces.
Here’s a test feature called DemoFeature3. The service “TestService” is associated to the feature. The service starts and stopped as the feature starts and stops.
Here is that service:
It’s not the most interesting method I’ve ever written. It has 2 simple parameters, and it simply displays them. Whoopty-doo. Notice, though, that there’s nothing special about this class. It has the optional NAME attribute (which is redundant in this case), but that’s it. It’s just a class. It doesn’t even have a service attribute. We know it’s a service because the feature said it is. (Although that may change going forward. Currently I only have one messaging pattern implemented. Attributes may be needed to specify things as more patterns are added.)
Also notice that there is only one method. That’s just laziness. You can put in as many as you want. At this time, though, you cannot have 2 methods of the same name. The binder is simplistic and would have to be improved to support overloads.
When DemoFeature3 starts, the service starts, and it will start listening for messages. The RabbitMQ implementation (which is completely hidden from this code) starts listening on the queue for this service. The queue name and exchange name are known by convention, and will be created as necessary.
What about the client? It’s RabbitMQ, so it’s just a message. You could submit a message from the RabbitMQ admin page if you’d like. Or, you can do it in code. If that code is in the app server (or using it’s client library), then it doesn’t know about RabbitMQ either. The message you send must be a JSON serialized ServiceRequest object.
Loose Typed Client
So far, I only have a loose typed client. I would like to create a strong typed one, but that’s going to require some thought and, ultimately, on-the-fly code generation. That’ll be a big chunk of work, and it’s not on my real short list, but I will get to it. (or, maybe you would like to get to it for me? Still looking for help people!)
The service name, operation name, service type, and parameters are all wrapped up into an object called ServiceRequest. The server picks that apart and uses the container and reflection to instantiate the class and invoke the method.
There is an API to do this. You can assemble a ServiceRequest yourself. It’s just a POCO with some fluent methods to make it… fluent. But, generally, that’s not necessary and you can use a simple helper method.
What follows is AnotherPeskyFeature. This was used previously to demonstrate the local scheduling feature. You put an attribute on a method that says “call me every second”, and the app server will call it every second.
I enhanced it to make as service call every time the method fires.
See that the service client is injected in the constructor. Now, you can use the service client to invoke the service: service name, operation name, parameter 1, parameter 2. This is the disadvantage of it being loose: Like non-hateoas REST you need to know what to call.
The IServiceClient interface does not have the method shown above. Instead, that is an extension method of IServiceClient that takes the parameters, converts them to a ServiceRequest, then calls the method that is defined by the interface. I did that because the implementation of that method will never change regardless of the backend technology, so there’s no reason to have multiple implementations doing exactly the same thing.
In this demo, both features are in the same application, which is hosted by a console app. Thus, you see each feature creating it’s output. Even though they are running next to each other, they don’t know that they are.. One feature is putting messages on the queue, and the other is taking them off.
If you deployed the application to multiple machines, then all of those machines would consume the same queue so that you can spread the workload.
There’s nothing to do on the hub. On the node:
I can easily add an extension method to do all 3 at once. The BUS and SERVICE LAYER have nothing to with each other, though. You can eliminated the UseRabbitMQBus call, and the buss will use it’s SignalR default. (I don’t have a second implementation of the service layer.)
Rabbit MQ Details
I’ve written my own quick API on top of the RabbitMQ API. “Quick” is the key word. I just need it to work. I can spend weeks working on that alone.
The exchange names and queue names are determined by convention. Currently, the client sets up the exchange and queue if needed, but I would prefer the hub do that on the node’s behalf to reduce the rights required by the nodes.
There are 4 sets of credentials: default, bus, service host, service client. If you don’t set specific credentials for any of them, then the default will be used.
Here’s what this stuff looks like in the portal:
The application feature “DemoFeature3” depends on core feature “ServiceHost”. Thus, it won’t let you shut down ServiceHost unless you shutdown DemoFeature3 first.
If both are stopped, then ServiceHost will start automatically when you start DemoFeature3. Sweet.
To host a service: add a ServiceHost attribute to the feature, and pass it the type of the service. You don’t concern yourself with RabbitMQ or any other technology that might be in play. As an application/feature developer, it’s not your concern.
To send to a service: Add an IServiceClient parameter to your constructor, then use it to invoke the service.
Request/Reply. While it should be avoided when possible, you know that you’re going to need it. The user will invoke the method. The service client will send the request and wait for a response. (In RabbitMQ, this is done via a one-time use response queue.)
Also, like I did for Web API, I will provide the option to have a service also be a feature. That way, if all you want is the service, you can do it via single class. That’s minor.