Skip to content

Commit

Permalink
Proper Akka.NET Dependency Injection: IRequiredActor<TActor> (#170)
Browse files Browse the repository at this point in the history
* created a proper DI implementation for actors

* completed end to end DI prototype

continuation of #144

* added negative test case
  • Loading branch information
Aaronontheweb authored Dec 27, 2022
1 parent 40671c2 commit a91b72d
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 3 deletions.
108 changes: 108 additions & 0 deletions src/Akka.Hosting.Tests/RequiredActorSpecs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using System;
using System.Threading.Tasks;
using Akka.Actor;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Xunit;

namespace Akka.Hosting.Tests;

public class RequiredActorSpecs
{
public sealed class MyActorType : ReceiveActor
{
public MyActorType()
{
ReceiveAny(_ => Sender.Tell(_));
}
}

public sealed class MyConsumer
{
private readonly IActorRef _actor;

public MyConsumer(IRequiredActor<MyActorType> actor)
{
_actor = actor.ActorRef;
}

public async Task<string> Say(string word)
{
return await _actor.Ask<string>(word, TimeSpan.FromSeconds(3));
}
}

public class MissingActor{}

public sealed class BadConsumer
{
private readonly IActorRef _actor;

public BadConsumer(IRequiredActor<MissingActor> actor)
{
_actor = actor.ActorRef;
}

public async Task<string> Say(string word)
{
return await _actor.Ask<string>(word, TimeSpan.FromSeconds(3));
}
}

[Fact]
public async Task ShouldRetrieveRequiredActorFromIServiceProvider()
{
// arrange
using var host = new HostBuilder()
.ConfigureServices(services =>
{
services.AddAkka("MySys", (builder, provider) =>
{
builder.WithActors((system, registry) =>
{
var actor = system.ActorOf(Props.Create(() => new MyActorType()), "myactor");
registry.Register<MyActorType>(actor);
});
});
services.AddScoped<MyConsumer>();
})
.Build();
await host.StartAsync();

// act
var myConsumer = host.Services.GetRequiredService<MyConsumer>();
var input = "foo";
var spoken = await myConsumer.Say(input);

// assert
spoken.Should().Be(input);
}

[Fact]
public async Task ShouldFailRetrieveRequiredActorWhenNotDefined()
{
// arrange
using var host = new HostBuilder()
.ConfigureServices(services =>
{
services.AddAkka("MySys", (builder, provider) =>
{
builder.WithActors((system, registry) =>
{
var actor = system.ActorOf(Props.Create(() => new MyActorType()), "myactor");
registry.Register<MyActorType>(actor);
});
});
services.AddScoped<BadConsumer>();
})
.Build();
await host.StartAsync();

// act
Action shouldThrow = () => host.Services.GetRequiredService<BadConsumer>();

// assert
shouldThrow.Should().Throw<MissingActorRegistryEntryException>();
}
}
36 changes: 33 additions & 3 deletions src/Akka.Hosting/ActorRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,40 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using Akka.Actor;
using Akka.Util;

namespace Akka.Hosting
{
/// <summary>
/// A strongly typed actor reference that can be used to send messages to an actor.
/// </summary>
/// <typeparam name="TActor">The type key of the actor - corresponds to a matching entry inside the <see cref="IActorRegistry"/>.</typeparam>
/// <remarks>
/// Designed to be used in combination with dependency injection to get references to specific actors inside your application.
/// </remarks>
public interface IRequiredActor<TActor>
{
/// <summary>
/// The underlying actor resolved via <see cref="ActorRegistry"/> using the given <see cref="TActor"/> key.
/// </summary>
IActorRef ActorRef { get; }
}

/// <summary>
/// INTERNAL API
/// </summary>
/// <typeparam name="TActor">The type key of the actor - corresponds to a matching entry inside the <see cref="IActorRegistry"/>.</typeparam>
public sealed class RequiredActor<TActor> : IRequiredActor<TActor>
{
public RequiredActor(IReadOnlyActorRegistry registry)
{
ActorRef = registry.Get<TActor>();
}

/// <inheritdoc cref="IRequiredActor{TActor}.ActorRef"/>
public IActorRef ActorRef { get; }
}

/// <summary>
/// INTERNAL API
/// </summary>
Expand Down Expand Up @@ -74,9 +105,8 @@ public MissingActorRegistryEntryException(string message, Exception innerExcepti
/// </remarks>
public class ActorRegistry : IActorRegistry, IExtension
{
private readonly ConcurrentDictionary<Type, IActorRef> _actorRegistrations =
new ConcurrentDictionary<Type, IActorRef>();

private readonly ConcurrentDictionary<Type, IActorRef> _actorRegistrations = new();

/// <inheritdoc cref="IActorRegistry.Register{TKey}"/>
/// <exception cref="DuplicateActorRegistryException">Thrown when the same value is inserted twice and overwriting is not allowed.</exception>
/// <exception cref="ArgumentNullException">Thrown when a <c>null</c> <see cref="IActorRef"/> is registered.</exception>
Expand Down
3 changes: 3 additions & 0 deletions src/Akka.Hosting/AkkaConfigurationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Akka.Serialization;
using Akka.Util;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;

namespace Akka.Hosting
Expand Down Expand Up @@ -351,6 +352,8 @@ internal void Bind()
{
return sp.GetRequiredService<ActorRegistry>();
});

ServiceCollection.AddSingleton(typeof(IRequiredActor<>), typeof(RequiredActor<>));
}

/// <summary>
Expand Down

0 comments on commit a91b72d

Please sign in to comment.