I am trying to grasp the fundamentals of raising/handling a Domain Event in my Solution. I am using Visual Studio 2017, .Net Core 1.1, C#, StructureMap 4.5.1.
The failure in my code came to light in a Unit Test which failed when checking if my Domain Event was being raised correctly.
My Startup.cs
class includes the following code:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddSingleton(_config);
services.AddAutoMapper();
services.AddMvc()
.AddControllersAsServices();
return ConfigureIoC(services);
}
public IServiceProvider ConfigureIoC(IServiceCollection services)
{
var container = new Container();
container.Configure(config =>
{
config.Scan(scan =>
{
scan.AssemblyContainingType(typeof(Startup));
scan.Assembly("Shared");
scan.Assembly("TaskScheduling");
scan.Assembly("TaskScheduling_Tests");
scan.WithDefaultConventions();
scan.ConnectImplementationsToTypesClosing(typeof(IHandle<>));
});
//Populate the container using the service collection
config.Populate(services);
});
return container.GetInstance<IServiceProvider>();
}
Where possible I have been following Udi Dahan's approach Domain Events - Salvation
My DomainEvents class implements the following Interface:
using System;
namespace Shared.Interfaces
{
public interface IDomainEvent
{
DateTime DateTimeEventOccurred { get; }
}
}
The DomainEvents class is as follows:
using System;
using System.Collections.Generic;
using Shared.Interfaces;
using StructureMap;
namespace Shared
{
/// <summary>
/// http://udidahan.com/2009/06/14/domain-events-salvation/
/// http://msdn.microsoft.com/en-gb/magazine/ee236415.aspx#id0400046
///
/// This class registers Domain Events and makes sure they get called.
/// </summary>
public static class DomainEvents
{
[ThreadStatic]
private static List<Delegate> actions;
public static IContainer Container { get; set; }
// Registers a callback for the given domain event.
public static void Register<T>(Action<T> callback) where T : IDomainEvent
{
if (actions == null)
{
actions = new List<Delegate>();
}
actions.Add(callback);
}
// Clears callbacks passed to Register on the current thread.
public static void ClearCallbacks()
{
actions = null;
}
// Raises the given domain event.
public static void Raise<T>(T args) where T : IDomainEvent
{
foreach (var handler in Container.GetAllInstances<IHandle<T>>())
{
handler.Handle(args);
}
if (actions != null)
{
foreach (var action in actions)
{
if (action is Action<T>)
{
((Action<T>)action)(args);
}
}
}
}
}
}
I have a Task
class which when updated raises a TaskUpdatedEvent
. The TaskUpdatedEvent
class is as follows:
using Shared.Interfaces;
using System;
namespace TaskScheduling.Model.Events
{
public class TaskUpdatedEvent : IDomainEvent
{
/// <summary>
/// When creating a TaskUpdatedEvent you have to pass in the Task object.
/// </summary>
/// <param name="task"></param>
public TaskUpdatedEvent(ScheduleAggregate.Task task)
: this()
{
TaskUpdated = task;
}
public TaskUpdatedEvent()
{
this.Id = Guid.NewGuid();
DateTimeEventOccurred = DateTime.Now; // IDomainEvent interface requirement.
}
public Guid Id { get; private set; }
public DateTime DateTimeEventOccurred { get; private set; }
public ScheduleAggregate.Task TaskUpdated { get; private set; }
}
}
and the event is raised with the following lines in my Task
class:
var taskUpdatedEvent = new TaskUpdatedEvent(this);
DomainEvents.Raise(taskUpdatedEvent);
I only have one Unit Test, so far, to check if this event is being raised. The Unit Test is as follows:
using System;
using NUnit.Framework;
using Shared;
using TaskScheduling.Model.ScheduleAggregate;
using TaskScheduling.Model.Events;
namespace TaskScheduling_Tests
{
[TestFixture]
public class TaskUpdatedEventShould
{
private Task testTask;
private readonly Guid testScheduleId = Guid.NewGuid();
private const int TestLocationId = 567;
private const int TestDeviceId = 123;
private const int TestTaskTypeId = 1;
private readonly DateTime testStartTime = new DateTime(2014, 7, 1, 9, 0, 0);
private readonly DateTime testEndTime = new DateTime(2014, 7, 1, 9, 30, 0);
private readonly DateTimeRange newTaskTimeRange = new DateTimeRange(new DateTime(2014, 6, 9, 10, 0, 0), TimeSpan.FromHours(1));
private const string TestTitle = "Unit Test Title";
[SetUp]
public void SetUp()
{
DomainEvents.ClearCallbacks();
testTask = Task.Create(
testScheduleId,
TestLocationId,
TestDeviceId,
TestTaskTypeId,
testStartTime,
testEndTime,
TestTitle
);
}
[Test]
public void EntityConstructor_IsNot_Null()
{
Assert.IsNotNull(testTask);
}
[Test]
public void RaiseTaskUpdatedEvent()
{
// Arrange
Guid updatedAppointmentId = Guid.Empty;
DomainEvents.Register<TaskUpdatedEvent>(aue =>
{
// This defines happens when the event is raised/
// The 'updatedAppointmentId' is changed from being all zeros to the testTask's id value.
updatedAppointmentId = testTask.Id;
});
// Act
testTask.UpdateTime(newTaskTimeRange);
// Assert
Assert.AreEqual(testTask.Id, updatedAppointmentId);
}
}
}
The failure appears to occur in the DomainEvent class when the Raise
method is called. Debugging shows that the event is raised and the arguments are set, however the Container is Null
so the foreach
loop cannot check for handlers.
I cannot figure out why the Container is Null
but I'm sure I must be missing something obvious. Any suggestions welcome.