Describe the bug
After upgrading from .net core 2.2 to 3.1, integration tests are failing.
All tests are wrapped in TransactionScope so that all changes to db should be revered (scope.Complete() is not called).
When call to the data access layer is made through api (HttpClient) records are created in the database, but they should not be since the entire test is wrapped in TransactionScope.
To Reproduce
public class Entity
{
public int Id { get; set; }
public string Name { get; set; }
}
public class CustomDbContext : DbContext
{
private const string DefaultConnectionString = "Server=.;Initial Catalog=WebApi;Trusted_Connection=True;";
private readonly string _connectionString;
public CustomDbContext() : this(DefaultConnectionString)
{
}
public CustomDbContext(string connectionString)
{
_connectionString = connectionString;
}
public DbSet<Entity> Entities { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(_connectionString);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new EntityConfiguration());
}
public async Task Save<TModel>(TModel model)
{
using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
{
Update(model);
await SaveChangesAsync();
scope.Complete();
}
}
}
public class EntityService : IEntityService
{
private readonly CustomDbContext _db;
public EntityService(CustomDbContext db)
{
_db = db;
}
public async Task Save(Entity model) => await _db.Save(model);
}
[ApiController]
[Route("[controller]")]
public class EntityController : ControllerBase
{
private readonly IEntityService _service;
public EntityController(IEntityService service)
{
_service = service;
}
[HttpPost]
public async Task<IActionResult> Save(Entity model)
{
await _service.Save(model);
return Ok();
}
}
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddScoped<CustomDbContext>();
services.AddScoped<IEntityService, EntityService>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
/// <summary>
/// Apply this attribute to your test method to automatically create a <see cref="TransactionScope"/>
/// that is rolled back when the test is finished.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class AutoRollbackAttribute : BeforeAfterTestAttribute
{
TransactionScope scope;
/// <summary>
/// Gets or sets whether transaction flow across thread continuations is enabled for TransactionScope.
/// By default transaction flow across thread continuations is enabled.
/// </summary>
public TransactionScopeAsyncFlowOption AsyncFlowOption { get; set; } = TransactionScopeAsyncFlowOption.Enabled;
/// <summary>
/// Gets or sets the isolation level of the transaction.
/// Default value is <see cref="IsolationLevel"/>.Unspecified.
/// </summary>
public IsolationLevel IsolationLevel { get; set; } = IsolationLevel.Unspecified;
/// <summary>
/// Gets or sets the scope option for the transaction.
/// Default value is <see cref="TransactionScopeOption"/>.Required.
/// </summary>
public TransactionScopeOption ScopeOption { get; set; } = TransactionScopeOption.Required;
/// <summary>
/// Gets or sets the timeout of the transaction, in milliseconds.
/// By default, the transaction will not timeout.
/// </summary>
public long TimeoutInMS { get; set; } = -1;
/// <summary>
/// Rolls back the transaction.
/// </summary>
public override void After(MethodInfo methodUnderTest)
{
scope.Dispose();
}
/// <summary>
/// Creates the transaction.
/// </summary>
public override void Before(MethodInfo methodUnderTest)
{
var options = new TransactionOptions { IsolationLevel = IsolationLevel };
if (TimeoutInMS > 0)
options.Timeout = TimeSpan.FromMilliseconds(TimeoutInMS);
scope = new TransactionScope(ScopeOption, options, AsyncFlowOption);
}
}
public class CustomWebApplicationFactory : WebApplicationFactory<Startup>
{
private const string TestDbConnectionString = "Server=.;Initial Catalog=WebApiTestDB_V3;Trusted_Connection=True;";
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
services.AddSingleton(_ => new CustomDbContext(TestDbConnectionString));
var sp = services.BuildServiceProvider();
var db = sp.GetRequiredService<CustomDbContext>();
db.Database.Migrate();
});
}
}
public class IntegrationTest : IClassFixture<CustomWebApplicationFactory>
{
protected readonly HttpClient _client;
protected readonly IServiceProvider _serviceProvider;
protected readonly CustomDbContext _db;
public IntegrationTest(CustomWebApplicationFactory factory)
{
_client = factory.CreateClient();
_serviceProvider = factory.Services.CreateScope().ServiceProvider;
_db = _serviceProvider.GetRequiredService<CustomDbContext>();
}
protected void DetachAll()
{
_db.ChangeTracker.Entries()
.ToList()
.ForEach(e => e.State = EntityState.Detached);
}
protected async Task<Entity> AddTestEntity()
{
var model = new Entity
{
Name = "test entity"
};
await _db.AddAsync(model);
await _db.SaveChangesAsync();
return model;
}
}
public static class HttpContentHelper
{
public static HttpContent GetJsonContent(object model) =>
new StringContent(JsonConvert.SerializeObject(model), Encoding.UTF8, "application/json");
}
[AutoRollback]
public class EntityIntegrationTest : IntegrationTest
{
private const string apiUrl = "/entity";
public EntityIntegrationTest(CustomWebApplicationFactory factory) : base(factory)
{
}
[Fact]
public async Task CanAdd()
{
// arrange
var model = new Entity
{
Name = "new entity"
};
var content = HttpContentHelper.GetJsonContent(model);
// act
var response = await _client.PostAsync(apiUrl, content);
// assert
response.EnsureSuccessStatusCode();
var result = await _db.Entities.FirstOrDefaultAsync();
Assert.Equal(model.Name, result.Name);
}
[Fact]
public async Task CanUpdate()
{
// arrange
var model = await AddTestEntity();
DetachAll(); // detach all entries because posting to api would create a new model, saving a new object with existing key throws entity already tracked exception
model.Name = "updated entity";
var content = HttpContentHelper.GetJsonContent(model);
// act
var response = await _client.PostAsync(apiUrl, content);
// assert
response.EnsureSuccessStatusCode();
var result = await _db.Entities.FirstOrDefaultAsync();
Assert.Equal(model.Id, result.Id);
Assert.Equal(model.Name, result.Name);
}
[Fact]
public async Task CannotInsertDuplicate()
{
// arrange
var entity = await AddTestEntity();
var model = new Entity
{
Name = entity.Name
};
var content = HttpContentHelper.GetJsonContent(model);
// act
var response = await _client.PostAsync(apiUrl, content);
// assert
var result = await response.Content.ReadAsStringAsync();
Assert.Contains("Cannot insert duplicate", result);
}
}
There are many files/classes involved so I've created a example repository
Example tests that are failing are in https://github.com/niksloter74/web-api-integration-test/tree/master/netcore3.1
Working example in .net core 2.2 https://github.com/niksloter74/web-api-integration-test/tree/master/netcore2.2
Direct test for service layer is working correctly
[AutoRollback]
public class EntityServiceTest : IntegrationTest
{
private readonly IEntityService service;
public EntityServiceTest(CustomWebApplicationFactory factory) : base(factory)
{
service = _serviceProvider.GetRequiredService<IEntityService>();
}
[Fact]
public async Task CanAdd()
{
// arrange
var model = new Entity
{
Name = "new entity"
};
// act
await service.Save(model);
// assert
var result = await _db.Entities.FirstOrDefaultAsync();
Assert.Equal(model.Name, result.Name);
}
[Fact]
public async Task CanUpdate()
{
// arrange
var model = await AddTestEntity();
model.Name = "updated entity";
// act
await service.Save(model);
// assert
var result = await _db.Entities.FirstOrDefaultAsync();
Assert.Equal(model.Id, result.Id);
Assert.Equal(model.Name, result.Name);
}
[Fact]
public async Task CannotInsertDuplicate()
{
// arrange
var entity = await AddTestEntity();
var model = new Entity
{
Name = entity.Name
};
// act
var ex = await Assert.ThrowsAnyAsync<Exception>(async () => await service.Save(model));
// assert
Assert.StartsWith("Cannot insert duplicate", ex.InnerException.Message);
}
}
This is by design but there’s a flag to get the old behavior back on TestServer called PreserveExecutionContext.
Here is an official discussion thread.
This line in IntegartionTest class fixed the problem
_factory.Server.PreserveExecutionContext = true;
I've also updated the repository
Related
Let's say I have several ASP.NET BackgroundServices and each is logging to its own scope/operation (OP1 and OP2).
public class MyBackgroundService1 : BackgroundService
{
private readonly ILogger<MyBackgroundService1> _logger;
public MyBackgroundService1(ILogger<MyBackgroundService1> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var activity = new Activity("OP1");
activity.Start();
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Hello from MyBackgroundService1");
await Task.Delay(5000, stoppingToken);
}
}
}
public class MyBackgroundService2 : BackgroundService
{
private readonly ILogger<MyBackgroundService2> _logger;
public MyBackgroundService2(ILogger<MyBackgroundService2> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var activity = new Activity("OP2");
activity.Start();
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Hello from MyBackgroundService2");
await Task.Delay(1000, stoppingToken);
}
}
}
Now I would like to use Blazor and want to display a table per operation with all corresponding logs.
Example output
OP1 Logs:
Hello from MyBackgroundService1
Hello from MyBackgroundService1
OP2 Logs:
Hello from MyBackgroundService2
Hello from MyBackgroundService2
How would I do that?
For this purpose, you need to create a log provider that stores the information in the database and then retrieves the information from the log table.
First, create a class to store logs in the database as follows:
public class DBLog
{
public int DBLogId { get; set; }
public string? LogLevel { get; set; }
public string? EventName { get; set; }
public string? Message { get; set; }
public string? StackTrace { get; set; }
public DateTime CreatedDate { get; set; }=DateTime.Now;
}
Now, We need to create a custom DBLogger. The DBLogger class inherits from the ILogger interface and has three methods, the most important of which is the Log method, which is actually called every time the Logger is called in the program. To read more about the other two methods, you can refer here.
public class DBLogger:ILogger
{
private readonly LogLevel _minLevel;
private readonly DbLoggerProvider _loggerProvider;
private readonly string _categoryName;
public DBLogger(
DbLoggerProvider loggerProvider,
string categoryName
)
{
_loggerProvider= loggerProvider ?? throw new ArgumentNullException(nameof(loggerProvider));
_categoryName= categoryName;
}
public IDisposable BeginScope<TState>(TState state)
{
return new NoopDisposable();
}
public bool IsEnabled(LogLevel logLevel)
{
return logLevel >= _minLevel;
}
public void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception exception,
Func<TState, Exception, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
if (formatter == null)
{
throw new ArgumentNullException(nameof(formatter));
}
var message = formatter(state, exception);
if (exception != null)
{
message = $"{message}{Environment.NewLine}{exception}";
}
if (string.IsNullOrEmpty(message))
{
return;
}
var dblLogItem = new DBLog()
{
EventName = eventId.Name,
LogLevel = logLevel.ToString(),
Message = $"{_categoryName}{Environment.NewLine}{message}",
StackTrace=exception?.StackTrace
};
_loggerProvider.AddLogItem(dblLogItem);
}
private class NoopDisposable : IDisposable
{
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
}
}
}
Now we need to create a custom log provider so that an instance of the above custom database logger (DBLogger) can be created.
public class DbLoggerProvider : ILoggerProvider
{
private readonly CancellationTokenSource _cancellationTokenSource = new();
private readonly IList<DBLog> _currentBatch = new List<DBLog>();
private readonly TimeSpan _interval = TimeSpan.FromSeconds(2);
private readonly BlockingCollection<DBLog> _messageQueue = new(new ConcurrentQueue<DBLog>());
private readonly Task _outputTask;
private readonly IServiceProvider _serviceProvider;
private bool _isDisposed;
public DbLoggerProvider(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
_outputTask = Task.Run(ProcessLogQueue);
}
public ILogger CreateLogger(string categoryName)
{
return new DBLogger(this, categoryName);
}
private async Task ProcessLogQueue()
{
while (!_cancellationTokenSource.IsCancellationRequested)
{
while (_messageQueue.TryTake(out var message))
{
try
{
_currentBatch.Add(message);
}
catch
{
//cancellation token canceled or CompleteAdding called
}
}
await SaveLogItemsAsync(_currentBatch, _cancellationTokenSource.Token);
_currentBatch.Clear();
await Task.Delay(_interval, _cancellationTokenSource.Token);
}
}
internal void AddLogItem(DBLog appLogItem)
{
if (!_messageQueue.IsAddingCompleted)
{
_messageQueue.Add(appLogItem, _cancellationTokenSource.Token);
}
}
private async Task SaveLogItemsAsync(IList<DBLog> items, CancellationToken cancellationToken)
{
try
{
if (!items.Any())
{
return;
}
// We need a separate context for the logger to call its SaveChanges several times,
// without using the current request's context and changing its internal state.
var scopeFactory = _serviceProvider.GetRequiredService<IServiceScopeFactory>();
using (var scope = scopeFactory.CreateScope())
{
var scopedProvider = scope.ServiceProvider;
using (var newDbContext = scopedProvider.GetRequiredService<ApplicationDbContext>())
{
foreach (var item in items)
{
var addedEntry = newDbContext.DbLogs.Add(item);
}
await newDbContext.SaveChangesAsync(cancellationToken);
// ...
}
}
}
catch
{
// don't throw exceptions from logger
}
}
[SuppressMessage("Microsoft.Usage", "CA1031:catch a more specific allowed exception type, or rethrow the exception",
Justification = "don't throw exceptions from logger")]
private void Stop()
{
_cancellationTokenSource.Cancel();
_messageQueue.CompleteAdding();
try
{
_outputTask.Wait(_interval);
}
catch
{
// don't throw exceptions from logger
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
try
{
if (disposing)
{
Stop();
_messageQueue.Dispose();
_cancellationTokenSource.Dispose();
}
}
finally
{
_isDisposed = true;
}
}
}
}
In the end, it is enough to call this custom log provider (DbLoggerProvider) in the Startup.cs or Program.cs class.
var serviceProvider = app.ApplicationServices.CreateScope().ServiceProvider;
loggerFactory.AddProvider(new DbLoggerProvider(serviceProvider));
From now on, every time we call the _logger.LogInformation("");, the log information will also be stored in the database.
Note: Because the number of calls to record logs in the database may be high, a concurrent queue is used to store logs.
If you like, you can refer to my repository that implements the same method.
In order to log the areas separately(scope/operation), you can create several different DBLoggers to store the information in different tables.
I'm seeding sql tables with the use of context factory, It shows up and there is an element in mssql, but when I write a controller, I get an empty list with no entries in the table. Could it perhaps be the inheritance that is causing an issue? Classical concert is inherited from Concert.
ConcertController:
public class ConcertController : Controller
{
private readonly AppDbContext _context;
public ConcertController(AppDbContext context)
{
_context = context;
}
public IActionResult Index()
{
var data = _context.Concerts.ToList(); //this variable is null
return View();
}
}
DbContext:
public class AppDbContext : DbContext
{
public virtual DbSet<Concert> Concerts { get; set; }
public virtual DbSet<Ticket> Tickets { get; set; }
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Party>().HasBaseType<Concert>();
modelBuilder.Entity<ClassicalConcert>().HasBaseType<Concert>();
base.OnModelCreating(modelBuilder);
}
}
Data seeding:
var factory = new AppContextFactory();
using var context = factory.CreateDbContext();
var builder = WebApplication.CreateBuilder(args);
await AddData();
async Task AddData()
{
ClassicalConcert mozart;
await context.AddRangeAsync(new[]
{
mozart = new ClassicalConcert()
{
PerformerName = "Some random dude",
TicketsCount= 1000,
PerformanceDate= DateTime.Now,
Location = "Rnd location",
Description = "some rnd desc",
ImageURL = "some https",
VoiceType = Centaurea.Data.Enums.VoiceTypes.Bass,
ConcertName = "Mozarts back",
ComposersName = "Mozart",
}
});
await context.SaveChangesAsync();
}
Context Factory:
public class AppContextFactory : IDesignTimeDbContextFactory<AppDbContext>
{
public AppDbContext CreateDbContext(string[] args = null)
{
var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
optionsBuilder.UseSqlServer(configuration["ConnectionStrings:DefaultConnection"]);
return new AppDbContext(optionsBuilder.Options);
}
}
Picture of the Debuger:
I created sample Web API project in ASP.Net-Core that uses InMemory Database and it shows weird behavior that memory usage not changed(decreased) when removing it from Database.
I confirmed by watching ".Net Core Host" process in TaskManager(taskmgr.exe) and I cannot find the reason.
I tested by swagger and my sample code is below.
Please tell me what I'm getting wrong or mistaking.
[Develop Environment]
Asp.Net Core(2.2.0)
EntityFrameworkCore.InMemory(2.2.4)
Swashbuckle.AspNetCore(4.0.1)
[ValuesController.cs]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
private MyDbContext _context;
public ValuesController(MyDbContext context)
{
_context = context;
}
// PUT api/values/5
[HttpPut("{id}")]
public void Put(int id)
{
// Create about 200 MB Data of string
var result = new Result()
{
Id = id,
Data = new string('*', 100 * 1024 * 1024)
};
// Push 200 MB Data to InMemoryDB and save changes
_context.Result.Add(result);
_context.SaveChanges();
}
// DELETE api/values/5
[HttpDelete("{id}")]
public void Delete(int id)
{
// find specific id result
var result = _context.Result.Find(id);
// if found, remove it and save changes
if (result != null)
{
// ** Memory usage not change when removing **
_context.Result.Remove(result);
_context.SaveChanges();
}
}
}
[MyDbContext.cs]
public class MyDbContext : DbContext
{
/// <summary>
/// DB context
/// </summary>
/// <param name="options"></param>
public MyDbContext(DbContextOptions<MyDbContext> options) : base(options)
{
}
/// <summary>
/// Data set of Some Result
/// </summary>
public DbSet<Result> Result { get; set; }
}
/// <summary>
/// Contains search result
/// </summary>
[Table("result")]
public class Result
{
/// <summary>
/// ID
/// </summary>
public int Id { get; set; }
/// <summary>
/// Data
/// </summary>
public string Data { get; set; }
}
[Program.cs]
public class Program
{
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
var context = services.GetRequiredService<MyDbContext>();
}
host.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
[Startup.cs]
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddDbContext<MyDbContext>(opt =>
opt.UseInMemoryDatabase("TestMemoryDb").UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v0.0.1", new Info
{
Title = "InMemoryDBTest",
Version = "v0.0.1",
Description = "InMemoryDBTest",
});
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v0.0.1/swagger.json", "InMemoryDBTest V0.0.1");
c.RoutePrefix = string.Empty;
});
app.UseHttpsRedirection();
app.UseMvc();
}
}
vishwas-trivedi confirmed that GC does collect the unused memory when forced called.
I will close this question.
Thank you vishwas-trivedi
https://github.com/aspnet/EntityFrameworkCore/issues/16398
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.
Does anyone have an implementation lying around of an auto-mock container using Rhino Mocks and NInject?
OK I built one myself using the Moq integration as a starting point. It is very simple. You need these 3 classes:
public class AutoMockingKernel : StandardKernel
{
private readonly IMockProvider _mockProvider;
public void Reset()
{
Components.Get<ICache>().Clear();
}
protected override bool HandleMissingBinding(Type service)
{
var isSelfBindable = TypeIsSelfBindable(service);
var binding = new Binding(service)
{
ProviderCallback = isSelfBindable
? StandardProvider.GetCreationCallback(service)
: _mockProvider.GetCreationCallback(),
IsImplicit = true
};
if (!isSelfBindable)
binding.ScopeCallback = ctx => null;
AddBinding(binding);
return true;
}
public AutoMockingKernel(IMockProvider mockProvider, INinjectSettings settings, params INinjectModule[] modules)
: base(settings, modules)
{
_mockProvider = mockProvider;
}
public AutoMockingKernel(IMockProvider mockProvider, params INinjectModule[] modules)
: base(modules)
{
_mockProvider = mockProvider;
}
}
internal class RhinoMockProvider : IProvider
{
public Type Type { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="RhinoMockProvider"/> class.
/// </summary>
public RhinoMockProvider(Type type)
{
Type = type;
}
public object Create(IContext context)
{
return MockRepository.GenerateMock(Type, Type.EmptyTypes);
}
}
public class RhinoAutoMockProvider : IMockProvider
{
public Func<IContext, IProvider> GetCreationCallback()
{
return ctx => new RhinoMockProvider(ctx.Request.Service);
}
}
You can then create an auto-mocking kernel in your unit test like this:
[Test]
public void Test()
{
var kernel = new AutoMockingKernel(new RhinoAutoMockProvider());
... etc
}
There is a RhinoMocks integration extension available.