Functions

Nexus functions are a lightweight way of executing a method on a service in the background. Sometimes you want to execute some method but you want it to run in the background. Either because the method takes a long time or because you want to make sure that it's retried until it completes successfully.

Let's take a simplified example:

public class OrderController : Controller
{
    private readonly IOrderConfirmationEmailService _orderConfirmationEmailService;

    public OrderController(IOrderConfirmationEmailService orderConfirmationEmailService)
    {
        _orderConfirmationEmailService = orderConfirmationEmailService;
    }

    public async Task<IActionResult> OrderPlaced(string orderId)
    {
        await _orderConfirmationEmailService.SendEmailForOrderAsync(orderId);

        return Json(new
        {
            Success = true,
        });
    }
}

We need to send the order confirmation email, but there's a couple of issues with this approach. What happens if SendEmailForOrderAsync() throws an error? And what if it takes a couple of seconds to complete? If it throws an exception we'll miss sending the email for that order. And if it takes a couple of seconds we'll slow down the experience for the end customer.

We can fix that by using a Nexus function instead, like this:

public class OrderController : Controller
{
    private readonly INexusFunction _nexusFunction;

    public OrderController(INexusFunction nexusFunction)
    {
        _nexusFunction = nexusFunction;
    }

    public async Task<IActionResult> OrderPlaced(string orderId)
    {
        await _nexusFunction.RunInBackgroundAsync<IOrderConfirmationEmailService>(x => x.SendEmailForOrderAsync(orderId));

        return Json(new
        {
            Success = true,
        });
    }
}

With this change we've moved the actual execution of the SendEmailForOrderAsync() method into the background (maybe even to a different server). When Nexus executes the method it will catch any exceptions that happen and retry the call later. You can control the retry behavior using the [NexusFunction] attribute which you can read about below.

Registering functions

The only thing you need to do is to call AddNexusFunctions() like this:

builder.Services.AddNexusFunctions();

By default Nexus will use SQLite as the backing store for function meta data, but you can change that by setting the DatabaseEngine option like this:

builder.Services.AddNexusFunctions(options =>
{
    options.DatabaseEngine = DatabaseEngine.Postgres; // Or SqlServer
});

After that you can use INexusFunction and start running methods in the background. Note that by default the application will also start to run functions in the background. If you want an application to only add functions but not run them you can set UseBackgroundService to false like this:

builder.Services.AddNexusFunctions(options =>
{
    options.UseBackgroundService = false;
});

You must have at least one application that runs functions so this is only if you have multiple applications where one or more is dedicated for background processing.

If you're not using a continously running server and instead using something like Azure Functions or Azure Container Instances that start on a schedule you can set UseBackgroundService to false and instead call INexusFunctionExecutorService.RunAllFunctionsPendingSinceLastCallAsync() from your entrypoint. That method will run all functions that have been scheduled to run since the last time that method was called. See the example Azure function trigger here.

Keeping processed functions

By default Nexus will store meta data about processed functions for one day in the database before deleting them. You can control this by setting AddNexusFunctions(options => options.KeepProcessedRunsFor = TimeSpan.Zero);. You can also set the retention per function using the [NexusFunction] attribute which you can read about below.

Service registration

You can use any service you want when calling RunInBackgroundAsync<TService>(). The only requirement is that the service is registered in the IServiceCollection. Which means that you can use interfaces as well as long as you register an implementation for it. So for the IOrderConfirmationEmailService example above you'd do something like this:

serviceCollection.AddSingleton<IOrderConfirmationEmailService, MyOrderConfirmationEmailService>();

Multi threading

If the same method on the same service is called multiple times Nexus can sometimes call the same service in multiple threads at the same time. Make sure that all services that you use with Nexus functions has thread safe implementations.

Method arguments

Nexus will automatically capture the values of arguments passed to the method like in the example above with the orderId argument. The only requirement is that it can only be values that can be serialized to JSON. Which means that you can pass strings, numbers, dates, and data objects to the service method. But you can't pass another service for example. If you have a method that requires another service as an argument you need to create a separate Nexus friendly method. Something like this:

public class ExampleService
{
    private readonly OtherService _service;

    public ExampleService(OtherService service)
    {
        _service = service;
    }

    public void ExampleMethod(OtherService service, string someString)
    {
        service.SomeMetod(someString);
    }

    public void ExampleMethod(string someString)
    {
        ExampleMethod(_service, someString);
    }
}

So instead of having Nexus call the ExampleMethod(OtherService service, string someString) method you'd let it call ExampleMethod(string someString). You can of course create a separate service that does this instead of having the method in the same service.

If you need to change the way arguments are serialized you can register a custom implementation of INexusFunctionArgumentsSerializer with the service provider.

Running a method after a delay

If you don't want to run the method as fast as possible you can call RunInBackgroundAfterAsync<TService>() instead of RunInBackgroundAsync<TService>(). Like this:

await _nexusFunction.RunInBackgroundAfterAsync<IOrderConfirmationEmailService>(TimeSpan.FromMinutes(10), x => x.SendEmailForOrderAsync(orderId));

Running a method until a condition is met

Sometimes you want to keep running a method until some condition is true such as polling an external system. Let's say that you don't want to send the order confirmation email until the customer on the order has been exported to the CRM.

You can use recursion to let a method called by Nexus schedule a new run, something like this:

public MyOrderConfirmationEmailService
{
    private readonly ICustomerService _customerService;
    private readonly IOrderService _orderService;
    private readonly INexusFunction _nexusFunction;

    public MyOrderConfirmationEmailService(ICustomerService customerService, IOrderService orderService, INexusFunction nexusFunction)
    {
        _customerService = customerService;
        _orderService = orderService;
        _nexusFunction = nexusFunction;
    }

    public async Task SendEmailForOrderAsync(string orderId)
    {
        var order = await _orderService.GetAsync(orderId);
        if (!await _customerService.ExistsAsync(order.CustomerNumber))
        {
            // Wait 5 minutes to see if the customer has been exported
            await _nexusFunction.RunInBackgroundAfterAsync<IOrderConfirmationEmailService>(TimeSpan.FromMinutes(5), x => x.SendEmailForOrderAsync(orderId));
        }
        else
        {
            // Customer exists, let's break the recursion and send the email
            await SendEmailAsync(order);
        }
    }

    private async SendEmailAsync(Order order)
    {
        // Send the email
    }
}

In this example Nexus will continuously call SendEmailForOrderAsync(orderId) every five minutes until the customer has been exported.

[NexusFunction] attribute

The [NexusFunction] attribute lets you control some aspects of how Nexus will run a method. You specify the attribute either on the service class/interface or on individual methods.

You can use [NexusFunction] to control for how long meta data about successful runs of a method is stored like this:

[NexusFunction(KeepProcessedRunFor = "10")]
public void ExampleMethod()
{

}

This will store all calls to this method for 10 days as the value for KeepProcessedRunFor must be a string that can be parsed to a TimeSpan using TimeSpan.Parse().

Retries

By default Nexus will retry a function if an exception is thrown. The default strategy is to use linear back-off where the first retry is after 5 seconds, the second retry is after 10 seconds, the third is fter 15 seconds, etc.

You can control the delay and the max number of retries globally by setting the RetryAfter and MaxRetries options like this:

builder.Services.AddNexusFunctions(options =>
{
    options.RetryAfter = TimeSpan.FromSeconds(10);
    options.MaxRetries = 5;
});

But you can also set the max retry count on a function level like this:

[NexusFunction(MaxRetries = 5)]
public void ExampleMethod()
{

}

By default Nexus will keep calling the method forever until it completes successfully without any max retry number.

You can change the retry strategy from linear back-off to either NexusFunctionRetries.RetryUntilSuccess or NexusFunctionRetries.ManualRetry. NexusFunctionRetries.RetryUntilSuccess will keep calling the method without any delay whereas NexusFunctionRetries.ManualRetry won't retry at all. You can trigger a retry manually through the Admin UI or the API.

[NexusFunction(Retries = NexusFunctionRetries.RetryUntilSuccess)]
public void ExampleMethod()
{

}

How Nexus Functions works

When you call RunInBackgroundAsync<TService>(x => x.SomeMethod(someArgument)) Nexus will use .NET reflection to gather information about what it is you want to run. The full name of the service type, the name of the method to use and the arguments passed.

This is then stored in the database in a row that looks something like this:

status service_type method parameter_types_json arguments_json run_at_utc
"Pending" "My.Namespace.IOrderConfirmationEmailService, MyAssembly" "SendEmailForOrderAsync" ["System.String, System"] ["123"] "2022-01-01T00:00:00"

Nexus will then read this data from the database and again use reflection to locate the .NET types needed and deserialize the arguments from JSON. It will then locate the service through the IServiceProvider and execute the method in a background service.

The type signature for RunInBackgroundAsync<TService>() is a lambda that gets the TService passed in and either returns a Task or nothing. So the type system allows you to write something like this:

await _nexusFunction.RunInBackgroundAsync<IOrderConfirmationEmailService>(x => orderId == "123" ? x.SendEmailForOrderAsync(orderId) : x.DoSomethingElseAsync(orderId));

Nexus will however throw an exception if you do this as the only allowed lambda to pass in is a method call on the service passed into the lambda.

You can think of it as a more readable and type safe way of writing this:

await _nexusFunction.RunInBackgroundAsync(typeof(IOrderConfirmationEmailService), nameof(IOrderConfirmationEmailService.SendEmailForOrderAsync), new object?[] { orderId });

Return value

The method you use in your lambda is allowed to return a value for synchronous methods and Task or Task<T> for asynchronous methods but Nexus won't do anything with the return value. If you need to do something with the return value you should wrap the method in another method that does something with it. If you want to call a method that returns false if it didn't go as expected you should wrap it in a method that throws an exception instead. Something like this:

public class ExampleService
{
    public bool TryDoSomething()
    {
        // Do something interesting here and return true/false
    }

    [NexusFunction]
    public void DoSomething()
    {
        if (!TryDoSomething())
        {
            throw new Exception("It didn't go as planned");
        }
    }
}

Renaming classes or methods

Since Nexus will store full type names such as My.Namespace.MyService, MyAssembly in the database it's important not to rename classes that are used as Nexus functions. If you rename such a class or method existing functions will fail and you need to update the database with the new type names.