Search code examples
asp.netakkamessagingakka.netloose-coupling

Using Akka.net with Asp.net on a Modular Monolith architecture


Iwould like to implement a rest service using Akka and Asp.net. Following the example here

I create my AkkaService containing the FooActor ref and a controller who transform the http request to a RunProcess message which is sent to the FooActor.

[Route("api/[controller]")]
[ApiController]
public class MyController : Controller
{

    private readonly ILogger<MyController> _logger;
    private readonly IAkkaService Service;

    public RebalancingController(ILogger<MyController> logger, IAkkaService bridge)
    {
        _logger = logger;
        Service = bridge;
    }
    [HttpGet]
    public async Task<ProcessTerminated> Get()
    {
        var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60));
        return await Service.RunProcess(cts.Token);
    }
}

    public class AkkaService : IAkkaService, IHostedService
{

    private ActorSystem ActorSystem { get; set; }
    public IActorRef FooActor { get; private set; }
    private readonly IServiceProvider ServiceProvider;


    public AkkaService(IServiceProvider sp)
    {
        ServiceProvider = sp;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        var hocon = ConfigurationFactory.ParseString(await File.ReadAllTextAsync("app.conf", cancellationToken));
        var bootstrap = BootstrapSetup.Create().WithConfig(hocon);
        var di = DependencyResolverSetup.Create(ServiceProvider);
        var actorSystemSetup = bootstrap.And(di);
        ActorSystem = ActorSystem.Create("AkkaSandbox", actorSystemSetup);
        // </AkkaServiceSetup>
         
        // <ServiceProviderFor>
        // props created via IServiceProvider dependency injection
        var fooProps = DependencyResolver.For(ActorSystem).Props<FooActor>();
        FooActor = ActorSystem.ActorOf(rebalProps.WithRouter(FromConfig.Instance), "foo");
        // </ServiceProviderFor>

        await Task.CompletedTask;
    }


    public async Task<ProcessTerminated> RunProcess(CancellationToken token)
    {
        return await FooActor.Ask<ProcessTerminated>(new RunProcess(), token);
    }


public FooActor(IServiceProvider sp)
    {
        _scope = sp.CreateScope();


        Receive<RunProcess>(x =>
        {

            var basketActor = Context.ActorOf(Props.Create<BarActor>(sp), "BarActor");
            basketActor.Tell(new BarRequest());
            _log.Info($"Sending a request to Bar Actor ");

        });

        Receive<BarResponse>(x =>
        {
           ...... Here I need to send back a ProcessTerminated message to the controller
        });


    }

Now, let's imagine the FooActor send a message to the BarActor telling him to perform a given task and wait the BarResponse. How could I send back the ProcessTerminated message to the controller?

Few points to take into considerations:

  1. I want to ensure no coupling between BarActor and FooActor. By example, I could add the original sender ActorRef to the BarRequest and BarResponse. But the BarActor musn't know about the fooActor and MyController. The structure of the messages an how the barActor respond should not be dependent of what the FooActor do with the BarResponse.
  2. In the example I only use BarActor, but we can imagine to have many different actors exchanging messages before returning the final result to the controller.

Solution

  • Nitpick: you should use Akka.Hosting and avoid creating this mock wrapper service around the ActorSystem. That will allow you to pass in the ActorRegistry directly into your controller, which you can use to then access FooActor without the need for additional boilerplate. See "Introduction to Akka.Hosting - HOCONless, "Pit of Success" Akka.NET Runtime and Configuration" video for a fuller explanation.

    Next: to send the ProcessTerminated message back to your controller you need to save the Sender (the IActorRef that points to the temporary actor created by Ask<T>, in this instance) during your Receive<RunProcess> and make sure that this value is available inside your Receive<BarResponse>.

    The simple ways to accomplish that:

    1. Store the Sender in a field on the FooActor, use behavior-switching while you wait for the BarActor to respond, and then revert back to your original behavior.
    2. Build a Dictionary<RunProcess, IActorRef> (the key should probably actually be some unique ID shared by RunProcess and BarResponse - a "correlation id") and reply to the corresponding IActorRef stored in the dictionary when BarResponse is received. Remove the entry after processing.
    3. Propagate the Sender in the BarRequest and BarResponse message payloads themselves.

    All three of those would work. If I thought there were going to be a large number of RunProcess requests running in parallel I'd opt for option 2.