Skip to content

hgjura/ServerTools.ServerCommands

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

ServerTools.ServerCommands


Report a Bug Β· Request a Feature . Ask a Question

License: MIT NuGet Badge PRs welcome Gitter

code with hearth by Herald Gjura

Table of Contents

About

ServerCommands facilitates running of units of code or commands remotely. It incorporates principles of messaging architectures used by most messaging tools and frameworks, like Azure Service Bus, AWS SQS, RabbitMQ, or Azure Storage Queues, Apache Kafka without any of the knowledge and configuration expertise to manage such installations and configurations.

The library is made of a Core, which is used as a stand-alone only when you want to create your own implementation of the library, and individual implementation for each service. Most commonly, one of these implementation libraries is used, depending on the platform and service that you would like to use.

Each platform/service comes with its pros and cons. By no means they are a major dependency. All messaging services work very similarly, and the choice to use Azure Storage is purely for simplicity and cost vs. something like Azure Service Bus, which has some enhanced underlying features. Azure Storage provides both storage and queueing service at a minimal cost. In future iterations, more versions of this library that work with all the other messaging services.

For a practical implementation of this library, and its features, look into this separate project Command Pattern With Queues.

Implementations

Built With

  • C# (NET 6.0)
  • NewtonSoft.Json
  • Microsoft.Extensions.Logging
  • DryIoc
  • Polly

Getting Started

Prerequisites

  • Basic understanding of the Messaging Architectures
  • Understanding of the Command pattern in software development

Installation

NuGet Badge

Add the NuGet package to all the projects you want to use it in.

Usage

To post a remote command you first will need to create a command (in your own code) that inherits from IRemoteCommand. The interface IRemoteCommand will ask you to implement two elements:

  • RequiresResponse: Is a property that returns a bool that indicates if this command will return a response or not. More about this in the extended documentation here. For now just return false.
  • ExecuteAsync: Is the method that gets executed remotely. This method accepts a parameters of type dynamic and another of type CommandMetadata:
    • command contains the context of the command, or the command parameters. Note that cannot be a mismatch between what the command expects to run and the properties of the command parameter. For example, this sample command (AddNumbersCommand) expects that properties Number1 and Number2 two to be present in the parameter command and be of type int. If they are not, this command will fail the execution.
    • CommandMetadata contains metadata about the command. This is filled it by the library and mostly contains various DateTime.UtcNow timestamps as the command travels through the systems and goes through the stages. This is available to you, but you don't have to do anything with it, unless you will delve into more expert use-cases of correlation, ordering and special dead letter queue handling.

This method returns a tuple of object of type (bool, Exception, dynamic, CommandMetadata) containing four items.

  • Item1: returns true/false depending if the command executed successfully or not.
  • Item2: in case Item1 is false, this returns the Exception object, otherwise null.
  • Item3: returns a dynamic object that contains the command context of the response. This must be populated (be not null if property RequiresResponse is set to true). Otherwise return null.
  • Item4: returns a CommandMetadata object that contains the metadata. This is when you may want to add additional metadata datapoints before returning it to the caller.

Also, all exceptions must be handled within the body of the ExecuteAsync. Remember, these commands are executed remotely and asynchronously. There will be nothing returned to you! An unhandled exception will simply put the command back in the queue, and it will be tried five (5) times and then it will be placed in a dead letter queue, where it will sit until you bring it back from there to handle it properly.

See example:

public class AddNumbersCommand : IRemoteCommand
{
    public bool RequiresResponse => true;

    public async Task<(bool, Exception, dynamic, CommandMetadata)> ExecuteAsync(dynamic command, CommandMetadata meta)
    {
    // must handle exceptions
        try
        {
            int n1 = (int)command.Number1;
            int n2 = (int)command.Number2;

            int result = n1 + n2;
      
            // set the first item to true indicating success, set the 2nd to null. If this returns a Response, the third parameter would be a Response object. Forth parameter is the metadata object that is returned back.
            return await Task.FromResult<(bool, Exception, dynamic, CommandMetadata)>((true, null, new { Result = result, Message = "Ok." }, meta));
        }
        catch (Exception ex)
        {
            // set the first item to false indicating failure, set second items to the Exception thrown. Third is null. Fourth parameter is the metadata object that is returned back.
            return await Task.FromResult<(bool, Exception, dynamic, CommandMetadata)>((false, ex, null, meta));
        }
    }
}

Now that you have the command ready to be executed, you will need to post it to be executed remotely.

In its simplest form, to post a command to the server it is simply a few lines of code (three steps).

  • step 1: create an instance of the CommandContainer. This is the IoC container that holds all registrations for the remote commands.
  • step 2: register the command you created above with the IoC container. In its simplest form, command have a parameterless constructor (like the sample above). But this is unrealistic as we want our commands to do rich and complex things, so go here to see how you can create commands with parameters and how to register them.
  • step 3: instantiate and initialize Commands store object. This is the command center for your remote commands. It only had a handful of methods though, as the complexity is hidden internally. You initialize it by calling the InitializeAsync() method. This requires two parameters:
    • first parameter, is the command container we just created
    • second parameter, is an implementation of the ConnectionOptions abstract class (for Azure Service Bus library is AzureServiceBusConnectionOptions, for Storage Queues is AzureServiceBusConnectionOptions. Construction parameters for each implementation are different, so inspect the class or look at documentation to find out when information to pass.
  • step 4: post the command
var _container = new CommandContainer();

_container.RegisterCommand<AddNumbersCommand>();

var c = await new CloudCommands().InitializeAsync(_container, new AzureStorageQueuesConnectionOptions(Configuration["StorageAccountName"], Configuration["StorageAccountKey"], 3, logger, QueueNamePrefix: "somequeueprefix"));

/// for Azure Service Bus, it looks like this
/// var c = await new AzureServiceBus.CloudCommands().InitializeAsync(_container, new AzureServiceBusConnectionOptions(Configuration["ASBConnectionString"], 3, logger, QueueNamePrefix: _queueNamePrefix));

_ = await c.PostCommandAsync<AddNumbersCommand>(new { Number1 = 2, Number2 = 3 });

Once you post the command, you will need to create an executing context to execute them. Usually this would run in an Azure Function, an AKS container, a Windows service, a commandline, or other, and would run in a loop or in a schedule.

To execute the commands queued in a server you will need to create an executing context (a Commands object) and register all the commands that are expected to have been registered remotely. Failure to register the commands that are queued would mean that the executing context will receive a command that it cannot recognize and process, and as such it will send it eventually to the dead letter queue.

Since in our sample there is only one command, we are doing the registration in-line. In your code, as you add more and more commands, you may want to maintain a utility function of class that register commands as you add them, and all their dependencies, and returns a fully registered command container object to be passed to the Commands.

The ExecuteCommandsAsync takes no parameters, and returns a tuple of object of type (bool, Exception, dynamic, CommandMetadata) containing three items.

  • Item1: returns true/false depending if the command executed all the commands in the queue successfully or not.
  • Item2: contains the number of commands executed.
  • Item3: returns a List<string> object that contains a list of all the exception messages that were thrown during the execution of commands.
var _container = new CommandContainer();

_container.RegisterCommand<AddNumbersCommand>();

var c = await new CloudCommands().InitializeAsync(_container, new AzureStorageQueuesConnectionOptions(Configuration["StorageAccountName"], Configuration["StorageAccountKey"], 3, logger, QueueNamePrefix: "somequeueprefix"));

var result = await c.ExecuteCommandsAsync();

//check if something was wrong or if any items were processed at all
Assert.IsTrue(!result.Item1);

//check if 1 or more items were processed
Assert.IsTrue(result.Item2 > 0);

//check if there was any errors
Assert.IsTrue(result.Item3.Count > 0); //This value keeps the list of error messages that were encountered. After retrying 5 times the command is moved to the deadletter queue.

And that's that!

Special use cases

Metadata

Every Message object contains a CommandMetadata object. This object collects primarily timestamps of various stages that the message goes through. This can help see a message through time and make certain decisions about it.

It also has a UniqueId that you can use to correlate the message with others. There is also a CustomMetadata of type Dictionary<string, object> so you can slip in your own metadata.

The Message object itself has two special properties DequeueCount and DlqDequeueCount, which are the number of times the message is read from the main queue, and the number of times the message is posted into the DLQ.

DequeueCount for the message is reset (to 0) every time the message is placed in the main queue. While DlqDequeueCount is retained throughout the lifecycle of the message, until is permanently removed from the system.

Note that the metadata is added to the message overall size.

Commands that require responses

Though rare, there are instances when you need to return a response after executing a comman. A response is a way for a system to alert another system that a command has been executed succesfully, and provide some feedback context to do additional work. By combining commands with responses you can creat a multistep workflow, if necessary.

For example, the AddNumbersCommand, which adds two numbers, let's say 2 + 3, generates a AddNumbersResponse that contains the result, or number 5. This is way too simplistic, but you get the point. The responses are generated and placed in a separate queue. You retrieve and execte responses, the same way you retreive and execute commands. In fact responses are just like commands, but they are attached to a command.

Here is some code on how to execute a command that generates a response, and then executing that response.

var _container = new CommandContainer();

_container.RegisterCommand<AddNumbersCommand, AddNumbersResponse>();

var c = await new CloudCommands().InitializeAsync(_container, new AzureStorageQueuesConnectionOptions(Configuration["StorageAccountName"], Configuration["StorageAccountKey"], 3, logger, QueueNamePrefix: "somequeueprefix"));

var result = await c.ExecuteCommandsAsync();

//check if something was wrong or if any items were processed at all
Assert.IsTrue(!result.Item1);

//check if 1 or more items were processed
Assert.IsTrue(result.Item2 > 0);

//check if there was any errors
Assert.IsTrue(result.Item3.Count > 0); //This value keeps the list of error messages that were encountered. After retrying 5 times the command is moved to the deadletter queue.


var responses = await c.ExecuteResponsesAsync();

//check if something was wrong or if any items were processed at all
Assert.IsTrue(!responses.Item1);

//check if 1 or more items were processed
Assert.IsTrue(responses.Item2 > 0);

//check if there was any errors
Assert.IsTrue(responses.Item3.Count > 0); //This value keeps the list of error messages that were encountered. After retrying 5 times the command is moved to the deadletter queue.

And this is the configuration of the commands and the accompaning response. Note that whatever context is returned as Item3 from the command, becomes the context going into the response. You dont have to create the response, the library creates the response and posts for you.

    public class AddNumbersCommand : IRemoteCommand
    {
        private ILogger logger;
        public AddNumbersCommand(ILogger logger)
        {
            this.logger = logger;
        }

        public bool RequiresResponse => true;

        public async Task<(bool, Exception, dynamic, CommandMetadata)> ExecuteAsync(dynamic command, CommandMetadata meta)
        {
            logger ??= new DebugLoggerProvider().CreateLogger("default");

            try
            {
                int n1 = (int)command.Number1;
                int n2 = (int)command.Number2;

                int result = n1 + n2;

                logger.LogInformation($"<< {n1} + {n2} = {n1 + n2} >>");

                return await Task.FromResult<(bool, Exception, dynamic, CommandMetadata)>((true, null, new { Result = result, Message = "Ok." }, meta));
            }
            catch (Exception ex)
            {
                //logger.LogError(ex.Message);
                throw ex;
                return await Task.FromResult<(bool, Exception, dynamic, CommandMetadata)>((false, ex, null, meta));
            }
            finally
            {

            }
        }
    }

    public class AddNumbersResponse : IRemoteResponse
    {
        private ILogger logger;
        public AddNumbersResponse(ILogger logger)
        {
            this.logger = logger;
        }
        public async Task<(bool, Exception, CommandMetadata)> ExecuteAsync(dynamic response, CommandMetadata metadata)
        {
            logger ??= new DebugLoggerProvider().CreateLogger("default");

            try
            {
                var r = (int) response.Result;
                var m = (string) response.Message;

                logger.LogInformation($"<< Result from the command is in: Result = {r} | Message = {m} >>");

                return await Task.FromResult<(bool, Exception, CommandMetadata)>((true, null, metadata));
            }
            catch (Exception ex)
            {
                logger.LogError(ex.Message);

                return await Task.FromResult<(bool, Exception, CommandMetadata)>((false, ex, metadata));
            }
            finally
            {

            }
        }
    }

Note that comands and responses are run separately and in a different context. You could easily priorotize commands in a more frequent context, and execute responses in a separate context that runs slower or has a lower prority.

Handle dead letter queues

In Messaging-based architectures and solutions, the concept of the deadletter queue is one of the most important and unusual concepts. All messaging solutions focus primarily in speed (scale) and asynchronicity. In order to achive these high traffic speeds and loads, they need a way to put messages aside if something happens to them, to reprocess them at a later date.

The deadletter queue is a place where messages and date are kept in suspended animation. Generally the lifespan of a message in the deadletter queue (DLQ) is rather short (a few minutes, maybe 15-20 minutes at most), just enough to give the system (the client context or subscriber) a chance to recover and try again. In some rare curcumstaces, when the system processes very little data and at longer intervals the messages in the DLQ are kept for a longer period, but this is the exception.

As messages are dormant in the DLQ, in suspended animation, we can't keep them there forever! You dont want the DLQ to turn into a graveyard for messages or a trashcan of unprocessed messages. In fact, yuo may want to start planning about what to do and how to handle the DLQ, before you start planning on how to handle messages in the main queue.

In handling the DLQ, there are usually two main patterns/strategies that are used to deal with or flush the messages in the queue:

  • Pattern 1. The most simple an intuitive. Used 90% of the time. You just simply take the messages and put tham back into the main queue for reprocessing. You need to make sure that you allow for a timespan (of lets say 15-20 minutes) before the messages are put back in the queue. The timespan is based in the confidence you have for whatever issue there was with the messages or the subcriber, it is solved within that timespan. Otherwise, if you do it to soon you risk creating an infinite loop of messging going into the queue and failing or expiring and back into the queue again.
  • Pattern 2. You create a separate context to process the DLQ exlusively. You decide there and than on what to do with the message, either process it, arhcive it or delete it, based on some condition of the message itself. So the message does not go back in the queue, but it makes a rather one-way trip to the DLQ and somewhere else form there.

Nothing stops you to use one or both of these patterns in your code. As you will see the library makes it easy to combine them both.

To handle the DLQ with this library you call the methods HandleCommandsDlqAsync() and HandleResponsesDlqAsync(). These two methods are implemented similarly, but obviously one handles the commands dlq and the other the responses dlq.

The HandleCommandsDlqAsync() and HandleResponsesDlqAsync() take two optonal parmeters, Func<Message, bool> ValidateProcessing = null and int timeWindowinMinutes = 1. The timeWindowinMinutes is obvious, it the window in time that the processor uses to keep the processing open. It defaults to 1 minute. The ValidateProcessing, will talk about this in a second.

To satisfy the Pattern 1 above, you simply call HandleCommandsDlqAsync() without passing any parameter. It will simply take all the messages in the DLQ, record a couple of items in the metadata object and put them back in the queue, and delete the orginal message form the DLQ. It ptractucally flushes the DLQ.

To satisfy the Pattern 2 above, you will need to have some logic to decide what to do with each message, prior to deciding if you want to put the message back in the queue. This can be a decision based on the metadata object that accompanies the message. For example, the Message object has a property called DlqDequeueCount, This is the number of iterations that a message makes form the main queue to the dlq. You may decide that after 3 iterations in the DLQ, than delete the message altogether, or archive it somewhere else. Or you can use the Metadata.CommandPostedOn to get the timestamp of the original message time (this is the time when the first message was posted), and decide to delete the message after a certain time. You would implement that this way:

public bool HandleDlqMessage(Message m)
{
  return m.DlqDequeueCount >= 2 ? false : true;
}
var dlq = await commands.HandleCommandsDlqAsync(HandleDlqMessage)

If the Func parameter returns false, after any custom logic and processing you decide, than the outcome is that the library will simply remove the message form the DLQ hence deleting forever (you may decide to log a message or archive the message somewhere else). If it returns true, than the library falls into the Pattern 1 for that message, and posts it into the main queue, and removes it from the DLQ.

As you can see this option allows you to satisfy both Pattern 1 or Pattern 2, or a combination thereof.

Here is the full code for this:

public void I1000_TestIntegrationWithAzureStorageQueues()
{
  _container
    .Use(logger)
    .RegisterResponse<AddNumbersCommand, AddNumbersResponse>();

    //_ = await commands.PostCommandAsync<AddNumbersCommand>(new { Number1 = 2, Number2 = 3 });

    //var result1 = await commands.ExecuteCommandsAsync();
    //var result2 = await commands.ExecuteResponsesAsync();

    var dlq1 = await commands.HandleCommandsDlqAsync(HandleDlqMessage);
    var dlq1 = await commands.HandleResponsesDlqAsync(HandleDlqMessage)
}


public bool HandleDlqMessage(Message m) 
{
  return m.DlqDequeueCount >= 2 || m.Metadata.CommandPostedOn < DateTime.UtcNow.AddMinutes(-30) ? false : true;
}

Process commands in order

This applies only to the Azure Service Bus library, ServerTools.ServerCommands.AzureServiceBus. Other libraries do not support this functionality.

Let us look at how we can accomplish this in the Azure Service Bus library.

Te begin with here are some notes about this functioanlity.

  • In general, features like ordering are not realy supported in messaging architectures. It goes against the concepts of maximum asynchronicity and maximum scalability, on which arhcitectures are build. Most cloud sevices, do not support this. Azure Service Bus, also does not natively support it. However, creative developers have come with ways to enable this. This is one of those implementations.
  • Though, this may look like a rock-solid turnkey solution, it is not. It uses a feature of the Azure Service Bus, session state, that is meant for something else, to make this happen.
  • It is meant for small ordered sets. So for example an ecommerce order, the requires the order items to be processed in the way they were enetered in a shopping cart, would fit the ideal scenario. An IoT set of signals, send by a device in a timeseries fashion, that could go into the millions it is not a good candiadate. The amount of precossing of such series would eventually overwhelm the service.

This feature is based on the Session feature of the Azure Service Bus service. Hence, you will need to create the queue in the Standart or Premier tier. The Basic tier will not work. Look carefully on the sample below on how the queue initial configuration is made and all its options. If you had created a queue prior, in the Basic tier, running the InitializeAsync() with the Standard flag will not chnage that queue. InitializeAsync will simple check if the queue with that name exists and skipt it. So you will need to drop the queueu manually and recreate it.

As mentioned, the feature uses Session to be implemented, more specifically Session State. Each Session in Azure Service Bus has a State that lives as long as that Session lives. Th library expect the commands to be passed with some extra flags: SessionId, Order, and IsLast. SessionId is a Guid type and is as the name implies the session id of the session. This will be the identifies that the service uses for the session state. Order is an integer and hold the order in which the command should be executed. It starts with 1. IsLast is the flag that indicates that this command is the last in the set, and instructs the library to complete the execution of the order set and close the session and its state. Otherwise the library will instruct the Azure Service Bus service to keep the Session and its State open forever.

For example, a shopping cart order set that has 10 items in it, would have these as parameters: SessionId would be the same for each command, a guid that you create and you hold on to until all items/commands for that order set have been posted. Order would be from 1 to 10. And, IsLast would be false for items 1-9. The default value for this is false so you dont have to pass this specifically if that is the case. And, it should be set to true for item number 10.

Instead of the PostOrderedCommandAsync method of the library you call the PostOrderedCommandAsync with the appropriate parameters.

All other instractions are the same as before.

Here is an example on how you can integrate this in your code.

try
{

    var _container = new CommandContainer();

    _container.RegisterCommand<AddNumbersCommand, AddNumbersResponse>();

    //need to cast from IServerCommand to the actual AzureServiceBus.CloudCommaands, 
    //since the ICommand does not have the PostOrderedCommandAsync() (yet), but the implementation of the Service Bus library has it
    var c = (ServerTools.ServerCommands.AzureServiceBus.CloudCommands) await new ServerTools.ServerCommands.AzureServiceBus.CloudCommands()
            .InitializeAsync(_container, 
                new AzureServiceBusConnectionOptions(
                  Configuration["ASBConnectionString"], 
                  AzureServiceBusTier.Standard, 
                  3, 
                  logger, 
                  DefaultMessageTimeToLive: TimeSpan.FromMinutes(2), 
                  QueueNamePrefix: _queueNamePrefix, 
                  MaxWaitTime: TimeSpan.FromSeconds(10), 
                  RequiresSession: true));


    var session1 = Guid.NewGuid();
    var session2 = Guid.NewGuid();

    _ = await c.PostOrderedCommandAsync<AddNumbersCommand>(new { Number1 = 4, Number2 = 0 }, session1, 4, false);
    _ = await c.PostOrderedCommandAsync<AddNumbersCommand>(new { Number1 = 2, Number2 = 0 }, session1, 2, false);
    _ = await c.PostOrderedCommandAsync<AddNumbersCommand>(new { Number1 = 3, Number2 = 0 }, session1, 3, false);
    _ = await c.PostOrderedCommandAsync<AddNumbersCommand>(new { Number1 = 5, Number2 = 0 }, session1, 5, true);
    _ = await c.PostOrderedCommandAsync<AddNumbersCommand>(new { Number1 = 1, Number2 = 0 }, session1, 1, false);

    _ = await c.PostOrderedCommandAsync<AddNumbersCommand>(new { Number1 = 4, Number2 = 0 }, session2, 4);
    _ = await c.PostOrderedCommandAsync<AddNumbersCommand>(new { Number1 = 2, Number2 = 0 }, session2, 2);
    _ = await c.PostOrderedCommandAsync<AddNumbersCommand>(new { Number1 = 3, Number2 = 0 }, session2, 3);
    _ = await c.PostOrderedCommandAsync<AddNumbersCommand>(new { Number1 = 5, Number2 = 0 }, session2, 5, true);
    _ = await c.PostOrderedCommandAsync<AddNumbersCommand>(new { Number1 = 1, Number2 = 0 }, session2, 1);


    var result = await c.ExecuteCommandsAsync();
    var result2 = await c.ExecuteResponsesAsync();

    Assert.IsTrue(result.Item1);
    Assert.IsTrue(result.Item2 == 10);
    Assert.IsTrue(result.Item3.Count == 0);

    Assert.IsTrue(result2.Item1);
    Assert.IsTrue(result2.Item2 == 10);
    Assert.IsTrue(result2.Item3.Count == 0);
}
catch (Exception ex)
{
    throw;
}

And that is it!

Aside from the fragility this functionality has with large numbers, here are a few others that I have noticed this does not respond well.

  • Missing orders. So let say the order set of commands you are posting has a missing element. For example, you wer supposed to send 5 commands in the order of 1 to 5 with, 5 being last. But you actually posted, 4, 3, 5 (last), 1. Command in order 2 is missing! So what is the library to do !?!? It executes command with order 1, but it cannot execute commands 3,4,5 because it is alway waiting for command 2. This is how you solve this.
    • These non-executable commands will sit there in limbo (in the Deferred list of the Azure Service Bus queue) until their expiratiion. And than they will go into the deadletter queue.
    • Actually they will be invisible forever, until you call the HandleCommandsDlqAsync and than they will immediately show up in the deadletter queue and then processed. This seesm like a bug of teh Service Bus, though the product team calls it a feature.
    • During the call of HandleCommandsDlqAsync you have the option to decide what to do with these partail orders.
    • Note that if you call the HandleCommandsDlqAsync without passing a callback function, you simply putting these commads back in the main queue. Which in this context does not make much sense, since they cannot be executed as they are missing the previous item on the set. They will end up back in the dead letter queue, and will do that a few times and then be deleted forever.
    • In this case you would call HandleCommandsDlqAsync(HandleDlqMessageForPartialOrders) where HandleDlqMessageForPartialOrders is your own function, of this signature bool HandleDlqMessageForPartialOrders(Message m), where you can decide what to do with such commands, you can decide to execute them regradless, you can archive them, log them somewher, etc.
    • Note that HandleDlqMessageForPartialOrders needs to return a bool after it runs. Value of false means that the message that is passed as parameter goes once more in th main queue, and true means the message is removed from the queue an dno further processing will occur.
  • Missing last. Another scenario is that of when you forget to pass the last item with the IsLast = true parameter.
    • The library will process the items in order, but I am not sure how the service will behave if the library cannot clear the sessions and session states.
    • This may cause an issue over time, if all all orders have the missing IsLast parameter, and there is a large volume of them.
    • Let say, this is for you to find out!

For more detailed documentation and more complex use cases head to the official documentation at the GitHub repo. If there are questions or request new feautures do not hesitate to post them there.

Roadmap

See the open issues for a list of proposed features (and known issues).

Top ten upcoming features

  • Adopt the library to work with other backend services
    • Azure Storage Queues
    • Azure Service Bus
    • AWS SQS
    • Apache Kafka
    • Rabbit MQ
  • Enable batching, batch processing and command correlations
  • Enable ordering and ordered processing through sessions

Support

Reach out to the maintainer at one of the following places:

Project assistance

If you want to say thank you or/and support active development of ServerTools.ServerCommands:

  • Add a GitHub Star to the project.
  • Tweet about the ServerTools.ServerCommands on your Twitter.
  • Write interesting articles about the project on Dev.to, Medium or personal blog.

Together, we can make ServerTools.ServerCommands better!

Contributing

First off, thanks for taking the time to contribute! Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make will benefit everybody else and are greatly appreciated.

We have set up a separate document containing our contribution guidelines.

Thank you for being involved!

Authors & contributors

The original setup of this repository is by Herald Gjura.

For a full list of all authors and contributors, check the contributor's page.

Security

ServerTools.ServerCommands follows good practices of security, but 100% security can't be granted in software. ServerTools.ServerCommands is provided "as is" without any warranty. Use at your own risk.

For more info, please refer to the security.

License

This project is licensed under the MIT license.

Copyright 2021 Herald Gjura

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

About

No description, website, or topics provided.

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Languages