ContosoUniversityDotNetCore-Pages SliceFixture with AutoMapper - unit-testing

My Create handler uses AutoMapper to map from command to entity.
public async Task<int> Handle(Command command, CancellationToken cancellationToken)
{
var entity = _mapper.Map<ExampleEntity>(command);
await _db.ExampleEntity.AddAsync(entity, cancellationToken);
await _db.SaveChangesAsync(cancellationToken);
return entity.Id;
}
My test looks like this
var command = new Create.Command
{
Name = "Example 1",
...
};
var id = await _fixture.SendAsync(command, mapper);
id.ShouldNotBeNull();
This is the SliceFixture
[CollectionDefinition(nameof(SliceFixture))]
public class SliceFixtureCollection : ICollectionFixture<SliceFixture> { }
public class SliceFixture : IAsyncLifetime
{
private readonly Checkpoint _checkpoint;
private readonly IConfiguration _configuration;
private readonly IServiceScopeFactory _scopeFactory;
private readonly WebApplicationFactory<Startup> _factory;
public SliceFixture()
{
_factory = new ContosoTestApplicationFactory();
_configuration = _factory.Services.GetRequiredService<IConfiguration>();
_scopeFactory = _factory.Services.GetRequiredService<IServiceScopeFactory>();
_checkpoint = new Checkpoint();
}
public class ContosoTestApplicationFactory : WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureAppConfiguration((_, configBuilder) =>
{
configBuilder.AddInMemoryCollection(new Dictionary<string, string>
{
{"ConnectionStrings:DefaultConnection", _connectionString}
});
});
}
private readonly string _connectionString = "";
}
public async Task ExecuteScopeAsync(Func<IServiceProvider, Task> action)
{
using var scope = _scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<SchoolContext>();
try
{
await dbContext.BeginTransactionAsync();
await action(scope.ServiceProvider);
await dbContext.CommitTransactionAsync();
}
catch (Exception)
{
dbContext.RollbackTransaction();
throw;
}
}
public async Task<T> ExecuteScopeAsync<T>(Func<IServiceProvider, Task<T>> action)
{
using var scope = _scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<SchoolContext>();
try
{
await dbContext.BeginTransactionAsync();
var result = await action(scope.ServiceProvider);
await dbContext.CommitTransactionAsync();
return result;
}
catch (Exception)
{
dbContext.RollbackTransaction();
throw;
}
}
public Task ExecuteDbContextAsync(Func<SchoolContext, Task> action) => ExecuteScopeAsync(sp => action(sp.GetService<SchoolContext>()));
public Task ExecuteDbContextAsync(Func<SchoolContext, ValueTask> action) => ExecuteScopeAsync(sp => action(sp.GetService<SchoolContext>()).AsTask());
public Task ExecuteDbContextAsync(Func<SchoolContext, IMediator, Task> action) => ExecuteScopeAsync(sp => action(sp.GetService<SchoolContext>(), sp.GetService<IMediator>()));
public Task<T> ExecuteDbContextAsync<T>(Func<SchoolContext, Task<T>> action) => ExecuteScopeAsync(sp => action(sp.GetService<SchoolContext>()));
public Task<T> ExecuteDbContextAsync<T>(Func<SchoolContext, ValueTask<T>> action) => ExecuteScopeAsync(sp => action(sp.GetService<SchoolContext>()).AsTask());
public Task<T> ExecuteDbContextAsync<T>(Func<SchoolContext, IMediator, Task<T>> action) => ExecuteScopeAsync(sp => action(sp.GetService<SchoolContext>(), sp.GetService<IMediator>()));
public Task InsertAsync<T>(params T[] entities) where T : class
{
return ExecuteDbContextAsync(db =>
{
foreach (var entity in entities)
{
db.Set<T>().Add(entity);
}
return db.SaveChangesAsync();
});
}
public Task<T> FindAsync<T>(int id)
where T : class, IEntity
{
return ExecuteDbContextAsync(db => db.Set<T>().FindAsync(id).AsTask());
}
public Task<TResponse> SendAsync<TResponse>(IRequest<TResponse> request)
{
return ExecuteScopeAsync(sp =>
{
var mediator = sp.GetRequiredService<IMediator>();
return mediator.Send(request);
});
}
public Task SendAsync(IRequest request)
{
return ExecuteScopeAsync(sp =>
{
var mediator = sp.GetRequiredService<IMediator>();
return mediator.Send(request);
});
}
private int _courseNumber = 1;
public int NextCourseNumber() => Interlocked.Increment(ref _courseNumber);
public Task InitializeAsync() => _checkpoint.Reset(_configuration.GetConnectionString("DefaultConnection"));
public Task DisposeAsync()
{
_factory?.Dispose();
return Task.CompletedTask;
}
}
When I run the test I get -
Object reference not set to an instance of an object.
because _mapper is null.
I know I probably need to modify the SliceFixture but can't figure out what to do.
I was looking to implement Jimmy’s example here with the additional AutoMapper implementation.

Related

How to Automoq concrete class dependencies through Automoq?

In my class constrctur, we had multiple concrete class dependencies. as per automoq documentation, we can only interface or abstraction.
System Under Test Class, in that ManageLocationRepository is concrete class dependency.
public class CityEventListener : IEvent<LocationChangeEventData>
{
private readonly ILocationAdapterCaller _locationAdapterCaller;
private readonly ManageLocationRepository _managerLocationRepository;
public CityEventListener(ILocationAdapterCaller locationAdapterCaller, ManageLocationRepository managerLocationRepository)
{
_locationAdapterCaller = locationAdapterCaller;
_managerLocationRepository = managerLocationRepository;
}
public async Task<bool> ProcessEvent(LocationChangeEventData eventData)
{
}
}
Test Case -
[Theory(DisplayName = "Valid value test")]
[ClassAutoMoqData(typeof(ValidValueTests))]
public async Task ProcessEvent_WithCreateOrUpdateOperation_CallsUpsertCityAndReturnsResult(ExpectedValueTestData<Parameters, bool> data,
[Frozen] Mock<ILocationAdapterCaller> locationAdapterCallerMock, [Frozen] Mock<ManageLocationRepository> managerLocationRepositoryMock,
CityEventListener sut)
{
// fakes
var cityDetail = _fixture.Build<CityDetail>()
.With(x => x.Id, data.Params.locationChangeEventData.Id).Create();
// Arrange
locationAdapterCallerMock.Setup(mock => mock.GetCityDetail(data.Params.locationChangeEventData.Id))
.ReturnsAsync(cityDetail).Verifiable();
managerLocationRepositoryMock
.Setup(mock => mock.UpsertCity(cityDetail))
.ReturnsAsync(data.ExpectedValue).Verifiable();
var result = await sut.ProcessEvent(data.Params.locationChangeEventData);
// Assert
using (new AssertionScope())
{
Assert.IsType<bool>(result);
Assert.Equal(data.ExpectedValue, result);
locationAdapterCallerMock.Verify();
managerLocationRepositoryMock.Verify();
}
}
ClassAutoMoq Attribute
public class ClassAutoMoqDataAttribute : CompositeDataAttribute
{
public ClassAutoMoqDataAttribute(Type values)
: base(new ClassDataAttribute(values), new AutoMoqDataAttribute())
{
}
}
public class AutoMoqDataAttribute : AutoDataAttribute
{
public AutoMoqDataAttribute() : base(() =>
{
var fixture = new Fixture().Customize(new CompositeCustomization(
new AutoMoqCustomization() { ConfigureMembers = true, GenerateDelegates = true },
new SupportMutableValueTypesCustomization()));
fixture.Behaviors.OfType<ThrowingRecursionBehavior>().ToList().ForEach(b => fixture.Behaviors.Remove(b));
fixture.Behaviors.Add(new OmitOnRecursionBehavior());
return fixture;
})
{
}
}
what are the alternate way to moq such dependencies through automoq attribute.
I solved my problem by taking two actions -
mark concrete class method virtual
create custom class automoq attribute to freeze dependencies
public class CityEventListenerClassAutoMoqAttribute : CompositeDataAttribute
{
public CityEventListenerClassAutoMoqAttribute(Type values)
: base(new ClassDataAttribute(values), new CityEventListenerAutoMoqAttribute())
{
}
}
public class CityEventListenerAutoMoqAttribute : AutoDataAttribute
{
public CityEventListenerAutoMoqAttribute()
: base(() =>
{
var fixture = new Fixture().Customize(new CompositeCustomization(
new AutoMoqCustomization() { ConfigureMembers = true, GenerateDelegates = true },
new SupportMutableValueTypesCustomization()));
var managerLocationRepositoryMock =
fixture.Freeze<Mock<ManageLocationRepository>>();
fixture.Inject(managerLocationRepositoryMock.Object);
return fixture;
})
{
}
}
Now my test case looks like this -
[Theory(DisplayName = "Valid value test")]
[CityEventListenerClassAutoMoq(typeof(ValidValueTests))]
public async Task ProcessEvent_WithCreateOrUpdateOperation_CallsUpsertCityAndReturnsResult(ExpectedValueTestData<Parameters, bool> data,
[Frozen] Mock<ILocationAdapterCaller> locationAdapterCallerMock, [Frozen] Mock<ManageLocationRepository> managerLocationRepositoryMock,
CityEventListener sut)
{
// fakes
var cityDetail = _fixture.Build<CityDetail>()
.With(x => x.Id, data.Params.locationChangeEventData.Id).Create();
// Arrange
locationAdapterCallerMock.Setup(mock => mock.GetCityDetail(data.Params.locationChangeEventData.Id))
.ReturnsAsync(cityDetail).Verifiable();
managerLocationRepositoryMock
.Setup(mock => mock.UpsertCity(cityDetail))
.ReturnsAsync(data.ExpectedValue).Verifiable();
var result = await sut.ProcessEvent(data.Params.locationChangeEventData);
// Assert
using (new AssertionScope())
{
Assert.IsType<bool>(result);
Assert.Equal(data.ExpectedValue, result);
locationAdapterCallerMock.Verify();
managerLocationRepositoryMock.Verify();
}
}
Do let me know if I can improve anything in my approach.

Mock DBSet<T> and call AsQueryable method return an empty list

I use entity framework core 6, XUnit for testing, Moq 4.16.1
I have a List, Mock it as DBSet<T>, I call AsQueryable method on DBSet<T> then return a empty list. If i mock AsQueryable method on DBSet<T>, i will have a expected list. Can you tell me Why?
This is my code
public static class MockDbExtensions
{
public static Mock<DbSet<T>> MockDbSet<T>(this IQueryable<T> testData) where T : class
{
Mock<DbSet<T>> dbSetMock = new Mock<DbSet<T>>();
dbSetMock.As<IQueryable<T>>().Setup(m => m.Provider).Returns(new TestAsyncQueryProvider<T>(testData.Provider));
dbSetMock.As<IQueryable<T>>().Setup(m => m.Expression).Returns(testData.Expression);
dbSetMock.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(testData.ElementType);
dbSetMock.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(() => testData.GetEnumerator());
dbSetMock.As<IAsyncEnumerable<T>>().Setup(m => m.GetAsyncEnumerator(default)).Returns(() => new TestAsyncEnumerator<T>(testData.GetEnumerator()));
return dbSetMock;
}
public static Mock<DbSet<T>> MockDbSet<T>(this IEnumerable<T> elements) where T : class
{
var testData = elements.AsQueryable();
return testData.MockDbSet();
}
public static Mock<DbSet<T>> MockDbSet<T>(this T[] elements) where T : class
{
var testData = elements.AsQueryable();
return testData.MockDbSet();
}
}
internal class TestAsyncQueryProvider<TEntity> : IAsyncQueryProvider
{
private readonly IQueryProvider _inner;
internal TestAsyncQueryProvider(IQueryProvider inner)
{
_inner = inner;
}
public IQueryable CreateQuery(Expression expression)
{
return new TestAsyncEnumerable<TEntity>(expression);
}
public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
{
return new TestAsyncEnumerable<TElement>(expression);
}
public object Execute(Expression expression)
{
return _inner.Execute(expression);
}
public TResult Execute<TResult>(Expression expression)
{
return _inner.Execute<TResult>(expression);
}
public IAsyncEnumerable<TResult> ExecuteAsync<TResult>(Expression expression)
{
return new TestAsyncEnumerable<TResult>(expression);
}
public TResult ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken = new CancellationToken())
{
var expectedResultType = typeof(TResult).GetGenericArguments()[0];
var executionResult = ((IQueryProvider)this).Execute(expression);
return (TResult)typeof(Task).GetMethod(nameof(Task.FromResult))
.MakeGenericMethod(expectedResultType)
.Invoke(null, new[] { executionResult });
}
}
internal class TestAsyncEnumerable<T> : EnumerableQuery<T>, IAsyncEnumerable<T>, IQueryable<T>
{
public TestAsyncEnumerable(IEnumerable<T> enumerable)
: base(enumerable)
{ }
public TestAsyncEnumerable(Expression expression)
: base(expression)
{ }
public IAsyncEnumerator<T> GetEnumerator()
{
return new TestAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
}
public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default)
{
return new TestAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
}
IQueryProvider IQueryable.Provider => new TestAsyncQueryProvider<T>(this);
}
internal class TestAsyncEnumerator<T> : IAsyncEnumerator<T>
{
private readonly IEnumerator<T> _inner;
public TestAsyncEnumerator(IEnumerator<T> inner)
{
_inner = inner;
}
public void Dispose()
{
_inner.Dispose();
}
public T Current => _inner.Current;
public Task<bool> MoveNext(CancellationToken cancellationToken)
{
return Task.FromResult(_inner.MoveNext());
}
public ValueTask<bool> MoveNextAsync() => new ValueTask<bool>(_inner.MoveNext());
public ValueTask DisposeAsync()
{
_inner.Dispose();
return new ValueTask();
}
}
public class ReportTemplateServiceTests
{
private List<ReportTemplateType> CreateReportTemplateTypes()
{
return new List<ReportTemplateType>
{
new ReportTemplateType { Id = new Guid("144013ED-2D77-4572-8AB4-483C66E4EF1D"), Name = "Financial statements", DisplayOrder = 3, CmsId = 101475, IsActive = true },
new ReportTemplateType { Id = new Guid("058F8F85-A735-48F3-B526-79855FFF1EB0"), Name = "Reports", DisplayOrder = 1, CmsId = 100473, IsActive = true },
new ReportTemplateType { Id = new Guid("69D55B31-69BB-4BBC-82F1-A707B484E99B"), Name = "Letters", DisplayOrder = 2, CmsId = 100474, IsActive = true },
new ReportTemplateType { Id = new Guid("19D2650B-CE8E-4B54-95D9-ACAFA5C1CE40"), Name = "Custom working papers", DisplayOrder = 4, CmsId = 101476, IsActive = true },
};
}
public async void CreateReportTemplateAsync_Success()
{
// Arrange
var reportTemplates = CreateReportTemplates().MockDbSet().Object;
var a = reportTemplates.AsQueryable().ToList(); // return empty list after call AsQueryable()
}
}
I mock AsQueryable method on DBSet<T>, i will have a expected list.

How to write a Unit testing for Azure Function app?

I have an azure function based on .Net core 3.1. It triggers when a file is uploaded into the Azure Blob Container, processes the CSV file and updates the database
Below is the code excerpt
FileTrigger.cs
namespace aspnetcore_azurefun_blob
{
[StorageAccount("AzureWebJobsStorage")]
public class FileTrigger
{
#region Property
private readonly IFileProcessor fileProcessor;
#endregion
#region Constructor
public FileTrigger(IFileProcessor fileProcessor)
{
this.fileProcessor = fileProcessor;
}
#endregion
[FunctionName("FileTrigger")]
public void ProcessFilesFromSamplesContainer([BlobTrigger("samples-workitems/{name}")]Stream myBlob, string name, ILogger log, ExecutionContext context)
{
log.LogInformation("Function: ProcessFilesFromSamplesContainer is called");
log.LogInformation($"C# Blob trigger function Processed blob\n Name:{name} \n Size: {myBlob.Length} Bytes");
var result = fileProcessor.ProcessAsync(myBlob, name, log, context);
log.LogInformation($"Function Completed {result.Result} on {DateTime.Now.ToLongDateString()} # {DateTime.Now.ToShortTimeString()}.\n.");
}
}
}
FileProcessor.cs
namespace ClientCore.Processor
{
public interface IFileProcessor
{
Task<string> ProcessAsync(Stream myBlob, string name, ILogger log, ExecutionContext context);
}
public class FileProcessor : IFileProcessor
{
#region Property
private readonly IFileProcessingServiceFacade fileProcessingService;
#endregion
#region Constructor
public FileProcessor(IFileProcessingServiceFacade fileProcessingService)
{
this.fileProcessingService = fileProcessingService;
}
#endregion
#region Public Methods
public async Task<string> ProcessAsync(Stream myBlob, string name, ILogger log, ExecutionContext context)
{
var processingResult = fileProcessingService.ProcessAsync(myBlob, name, log, context);
return await Task.FromResult($"{processingResult.Result}");
}
#endregion
}
}
FileProcessingService.cs
namespace BusinessService.Services
{
public interface IFileProcessingServiceFacade
{
Task<string> ProcessAsync(Stream myBlob, string name, ILogger log, ExecutionContext context);
}
partial class FileProcessingServiceFacade : IFileProcessingServiceFacade
{
#region Public Methods
public async Task<string> ProcessAsync(Stream myBlob, string name, ILogger log, ExecutionContext context)
{
// Get the Contents from the CSV File
var csvData = GetCSVData(myBlob);
AppDbContext.FileRecords.Add(new FileRecords
{
FileName = name,
IsCompleted = DefaultValues.IsCompleted
});
AppDbContext.SaveChanges();
log.LogInformation($"Reading configuration from the configuration settings file: {Configurator.AzureSQLServerConfigurator.ConnnectionString}");
log.LogInformation("Database is updated..!");
return await Task.FromResult($"success");
}
#endregion
}
partial class FileProcessingServiceFacade : ServiceBase<FileProcessingServiceFacade>
{
#region Constructor
public FileProcessingServiceFacade(AppDbContext appDbContext, IOptions<AppConfigurator> configurator)
: base(appDbContext, configurator) { }
#endregion
#region Extract Data from CSV
protected static List<dynamic> GetCSVData(Stream jobData)
{
try
{
#region CsvHelper Mapping and Conversion
var csvHelperConfig = new CsvHelper.Configuration.CsvConfiguration(CultureInfo.InvariantCulture)
{
NewLine = Environment.NewLine,
PrepareHeaderForMatch = args => args.Header.ToUpper().Replace(" ", ""),
HasHeaderRecord = true
};
using var reader = new StreamReader(jobData);
using var csvHelper = new CsvReader(reader, CultureInfo.InvariantCulture);
csvHelper.Context.RegisterClassMap<StudentDetailsMap>();
var csvHelperRrecords = csvHelper.GetRecords<StudentDetails>()
.Where(x => !string.IsNullOrWhiteSpace(x.Name))
.OrderBy(x => x.ID);
return csvHelperRrecords.Cast<dynamic>().ToList();
#endregion
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
}
#endregion
}
}
How to write a Unit testing for the above Azure Function app? Just a head start is required.

How to configure retry for MassTransit sagas

I'm having a concurrency issue with MassTransit sagas.
I'm currently working on a POC with this flow:
One thread produces 100 event that are published to MassTransit in a sequence.
When the saga is instantiated it publishes another event to MassTransit.
The new event is picked up by a Consumer that perform some business logic and publishes one of two resulting event to MassTransit.
The resulting events from step 3. triggers a state change in the saga
I sometimes get exceptions like this Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded. in step 4, and the state change is not persisted.
Here is the business logic code:
public interface IInitialSagaEvent : CorrelatedBy<Guid> { }
public interface IExternalCheckRequest : CorrelatedBy<Guid> { }
public interface IExternalCheckOk : CorrelatedBy<Guid> { }
public interface IExternalCheckNotOk : CorrelatedBy<Guid> { }
public class MySaga : SagaStateMachineInstance
{
public Guid CorrelationId { get; set; }
public string CurrentState { get; set; }
public byte[] RowVersion { get; set; }
}
public class MyStateMachine : MassTransitStateMachine<MySaga>
{
public MyStateMachine()
{
InstanceState(instance => instance.CurrentState);
Initially(
When(InitialSagaEvent)
.ThenAsync(context => context.GetPayload<ConsumeContext>().Publish<IExternalCheckRequest>(new { context.Instance.CorrelationId }))
.TransitionTo(AwaitingExternalCheck)
);
During(AwaitingExternalCheck,
Ignore(InitialSagaEvent),
When(ExternalCheckOk)
.TransitionTo(CheckedOk),
When(ExternalCheckNotOk)
.TransitionTo(CheckedNotOk)
);
During(CheckedOk,
When(InitialSagaEvent)
.ThenAsync(context => context.GetPayload<ConsumeContext>().Publish<IExternalCheckRequest>(new { context.Instance.CorrelationId }))
.TransitionTo(AwaitingExternalCheck)
);
During(CheckedNotOk,
When(InitialSagaEvent)
.ThenAsync(context => context.GetPayload<ConsumeContext>().Publish<IExternalCheckRequest>(new { context.Instance.CorrelationId }))
.TransitionTo(AwaitingExternalCheck)
);
}
public Event<IInitialSagaEvent> InitialSagaEvent { get; private set; }
public Event<IExternalCheckOk> ExternalCheckOk { get; private set; }
public Event<IExternalCheckNotOk> ExternalCheckNotOk { get; private set; }
public State AwaitingExternalCheck { get; private set; }
public State CheckedOk { get; private set; }
public State CheckedNotOk { get; private set; }
}
public class ExternalCheckRequestConsumer : IConsumer<IExternalCheckRequest>
{
private readonly IExternalChecker externalChecker;
public ExternalCheckRequestConsumer(IExternalChecker externalChecker)
{
this.externalChecker = externalChecker;
}
public async Task Consume(ConsumeContext<IExternalCheckRequest> context)
{
var ok = await externalChecker.PerformCheck(context.Message, context.CancellationToken);
if (ok)
{
await context.Publish<IExternalCheckOk>(new { context.Message.CorrelationId }, context.CancellationToken);
}
else
{
await context.Publish<IExternalCheckNotOk>(new { context.Message.CorrelationId }, context.CancellationToken);
}
}
}
public interface IExternalChecker
{
Task<bool> PerformCheck(IExternalCheckRequest request, CancellationToken cancellationToken);
}
public class Publisher
{
private readonly IPublishEndpoint publishEndpoint;
public Publisher(IPublishEndpoint publishEndpoint)
{
this.publishEndpoint = publishEndpoint;
}
public async Task Publish(Guid correlationId, CancellationToken cancellationToken)
{
await publishEndpoint.Publish<IInitialSagaEvent>(new { CorrelationId = correlationId }, cancellationToken);
}
}
Here it the configuration code
public class MySagaDbContext : SagaDbContext
{
public MySagaDbContext(DbContextOptions<MySagaDbContext> options) : base(options) { }
protected override IEnumerable<ISagaClassMap> Configurations
{
get
{
yield return new MySagaClassMap();
}
}
}
public class MySagaClassMap : SagaClassMap<MySaga>
{
protected override void Configure(EntityTypeBuilder<MySaga> entity, ModelBuilder model)
{
entity.Property(x => x.CurrentState).HasMaxLength(128);
entity.Property(x => x.RowVersion).IsRowVersion();
}
}
public class ExternalCheckRequestConsumerDefinition : ConsumerDefinition<ExternalCheckRequestConsumer>
{
protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator<ExternalCheckRequestConsumer> consumerConfigurator) =>
endpointConfigurator.UseRetry(r =>
{
r.Handle<DbUpdateConcurrencyException>();
// This is the SQLServer error code for duplicate key
r.Handle<DbUpdateException>(y => y.InnerException is SqlException e && e.Number == 2627);
r.Interval(5, TimeSpan.FromMilliseconds(100));
});
}
public class Program
{
public static async Task Main(string[] args)
{
var services = new ServiceCollection();
services.AddDbContext<DbContext, MySagaDbContext>((provider, builder)
=> builder.UseSqlServer(connectionString, m =>
{
m.MigrationsAssembly(typeof(MySagaDbContext).Assembly.GetName().Name);
m.MigrationsHistoryTable($"__EFMigrationsHistory_Sagas");
}));
services.AddMassTransit(configureMassTransit =>
{
configureMassTransit.AddConsumer<ExternalCheckRequestConsumer, ExternalCheckRequestConsumerDefinition>();
configureMassTransit.AddSagaStateMachine<MyStateMachine, MySaga>()
.EntityFrameworkRepository(r =>
{
r.ConcurrencyMode = ConcurrencyMode.Optimistic;
r.ExistingDbContext<MySagaDbContext>();
});
configureMassTransit.SetEndpointNameFormatter(new DefaultEndpointNameFormatter(true));
configureMassTransit.UsingActiveMq((context, config) =>
{
config.Host("artemis", 61616, configureHost =>
{
configureHost.Username("admin");
configureHost.Password("admin");
});
config.UseInMemoryOutbox(); // ref https://masstransit-project.com/articles/outbox.html#the-in-memory-outbox
config.EnableArtemisCompatibility();
config.ConfigureEndpoints(context);
});
});
var serviceProvider = services.BuildServiceProvider();
var busControl = serviceProvider.GetRequiredService<IBusControl>();
await busControl.StartAsync();
await RunPoc(serviceProvider);
}
private static async Task RunPoc(IServiceProvider serviceProvider)
{
await Task.CompletedTask;
}
static string connectionString = string.Empty;
}
My guess is that I need to get in a UseRetry at the correct point, so I've tried to configure the AddSagaStateMachine with UseRetry like this:
configureMassTransit.AddSagaStateMachine<MyStateMachine, MySaga>(
configure =>
{
configure.UseRetry(r =>
{
r.Handle<DbUpdateConcurrencyException>();
// This is the SQLServer error code for duplicate key
r.Handle<DbUpdateException>(y => y.InnerException is SqlException e && e.Number == 2627);
r.Interval(5, TimeSpan.FromMilliseconds(100));
});
})
.EntityFrameworkRepository(r =>
{
r.ConcurrencyMode = ConcurrencyMode.Optimistic;
r.ExistingDbContext<MySagaDbContext>();
});
But with this UseRetry in AddSagaStateMachine nothing works, I just get loads of exception like this:
fail: MassTransit.ReceiveTransport[0]
R - FAULT activemq://artemis:61616/XXXX
System.ArgumentException: THe message could not be retrieved: IInitialSagaEvent(Parameter 'context')
at MassTransit.Saga.Pipeline.Pipes.SagaMergePipe`2.Send(SagaConsumeContext`1 context)
at GreenPipes.Filters.RetryFilter`1.GreenPipes.IFilter<TContext>.Send(TContext context, IPipe`1 next)
at GreenPipes.Filters.RetryFilter`1.GreenPipes.IFilter<TContext>.Send(TContext context, IPipe`1 next)
at MassTransit.Saga.SendSagaPipe`2.Send(SagaRepositoryContext`2 context)
at MassTransit.Saga.SendSagaPipe`2.Send(SagaRepositoryContext`2 context)
at MassTransit.EntityFrameworkCoreIntegration.Saga.Context.EntityFrameworkSagaRepositoryContextFactory`1.<> c__DisplayClass5_0`1.<< Send > b__1 > d.MoveNext()
-- - End of stack trace from previous location ---
at MassTransit.EntityFrameworkCoreIntegration.Saga.Context.EntityFrameworkSagaRepositoryContextFactory`1.<> c__DisplayClass8_0.<< WithinTransaction > g__Create | 0 > d.MoveNext()
-- - End of stack trace from previous location ---
at MassTransit.EntityFrameworkCoreIntegration.Saga.Context.EntityFrameworkSagaRepositoryContextFactory`1.WithinTransaction[T](DbContext context, CancellationToken cancellationToken, Func`1 callback)
at MassTransit.EntityFrameworkCoreIntegration.Saga.Context.EntityFrameworkSagaRepositoryContextFactory`1.WithinTransaction[T](DbContext context, CancellationToken cancellationToken, Func`1 callback)
at MassTransit.EntityFrameworkCoreIntegration.Saga.Context.EntityFrameworkSagaRepositoryContextFactory`1.WithinTransaction[T](DbContext context, CancellationToken cancellationToken, Func`1 callback)
at MassTransit.EntityFrameworkCoreIntegration.Saga.Context.EntityFrameworkSagaRepositoryContextFactory`1.Send[T](ConsumeContext`1 context, IPipe`1 next)
at MassTransit.EntityFrameworkCoreIntegration.Saga.Context.EntityFrameworkSagaRepositoryContextFactory`1.Send[T](ConsumeContext`1 context, IPipe`1 next)
at MassTransit.ExtensionsDependencyInjectionIntegration.ScopeProviders.DependencyInjectionSagaRepositoryContextFactory`1.<> c__DisplayClass6_0`1.<< Send > g__CreateScope | 0 > d.MoveNext()
-- - End of stack trace from previous location ---
at MassTransit.ExtensionsDependencyInjectionIntegration.ScopeProviders.DependencyInjectionSagaRepositoryContextFactory`1.<> c__DisplayClass6_0`1.<< Send > g__CreateScope | 0 > d.MoveNext()
-- - End of stack trace from previous location ---
at MassTransit.Saga.Pipeline.Filters.CorrelatedSagaFilter`2.GreenPipes.IFilter<MassTransit.ConsumeContext<TMessage>>.Send(ConsumeContext`1 context, IPipe`1 next)
I'm using .Net 6 and have tried MassTransit v 7.3.1 and v 8.0.0-develop.391, but both has the same behavior.
I've tried defining the messages as interfaces and publishing them both as anonymous classes and as actual implementations, and also tried to define the messages as classes, but with no luck.
My hope it that there is just some small configuration detail I'm missing, but I'm out of ideas, so any help is deeply appreciated.
The proper configuration in your SagaDefinition is shown below. Note the use of UseMessageRetry, instead of UseRetry.
public class ExternalCheckRequestConsumerDefinition :
ConsumerDefinition<ExternalCheckRequestConsumer>
{
protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator,
IConsumerConfigurator<ExternalCheckRequestConsumer> consumerConfigurator) =>
endpointConfigurator.UseMessageRetry(r =>
{
r.Handle<DbUpdateConcurrencyException>();
// This is the SQLServer error code for duplicate key
r.Handle<DbUpdateException>(y => y.InnerException is SqlException e && e.Number == 2627);
r.Interval(5, TimeSpan.FromMilliseconds(100));
});
}
UPDATE
The above Consumer definition isn't used by the saga. You'd need to create a Saga definition, and specify it when adding the saga, for the retry to apply to the saga. Which would do the same as configuring it inline when adding the saga:
.AddSagaStateMachine<MyStateMachine, MySaga, MySagaDefinition>(
Also, in your state machine, replace the overly noisy:
.ThenAsync(context => context.GetPayload<ConsumeContext>().Publish<IExternalCheckRequest>(new { context.Instance.CorrelationId }))
With:
.PublishAsync(context => context.Init<IExternalCheckRequest>(new { context.Instance.CorrelationId }))
Here is the .AddSagaStateMachine I used, ref Chris Pattersons solution in the other answer.
configureMassTransit.AddSagaStateMachine<MyStateMachine, MySaga>(
configure =>
{
configure.UseMessageRetry(r =>
{
r.Handle<DbUpdateConcurrencyException>();
// This is the SQLServer error code for duplicate key
r.Handle<DbUpdateException>(y => y.InnerException is SqlException e && e.Number == 2627);
r.Interval(5, TimeSpan.FromMilliseconds(100));
});
})
.EntityFrameworkRepository(r =>
{
r.ConcurrencyMode = ConcurrencyMode.Optimistic;
r.ExistingDbContext<MySagaDbContext>();
});

.net core xunit How verify if method was called from inherited class

I have a base class called BaseService, and i want to verify if method IsValid was called in my unit test.
public interface IBaseService
{
bool IsValid<Dto, DtoValidator>(Dto entityDto, DtoValidator validator) where DtoValidator : AbstractValidator<Dto>;
}
public class BaseService : IBaseService
{
protected readonly IMapper _mapper;
protected readonly INotificationService _notification;
public BaseService(
IMapper mapper,
INotificationService notification)
{
_mapper = mapper;
_notification = notification;
}
public virtual bool IsValid<Dto, DtoValidator>(Dto entityDto, DtoValidator validator) where DtoValidator : AbstractValidator<Dto>
{
var result = validator.Validate(entityDto);
foreach (var error in result.Errors)
{
_notification.Notify(error.ErrorMessage);
}
return result.IsValid;
}
}
Class that use BaseService , some parts of code was omited
public interface IAfiliadoService : IBaseService {}
public class AfiliadoService : BaseService, IAfiliadoService
{
public AfiliadoService(
IMapper mapper,
INotificationService notification) : base(mapper, notification)
{
_afiliadoRepository = afiliadoRepository;
_lojaRepository = lojaRepository;
}
public async Task<AfiliadoResponseDto> AddAsync(AddAfiliadoRequestDto request)
{
if (IsValid(request, new AddAfiliadoRequestDtoValidator()))
{
}
}
}
In my test project i created those classes
public class MockBaseService
{
public readonly Mock<IMapper> _mapper;
public readonly Mock<INotificationService> _notification;
public MockBaseService()
{
_mapper = new Mock<IMapper>();
_notification = new Mock<INotificationService>();
}
}
public class AfiliadoServiceTest : MockBaseService
{
private readonly IAfiliadoService _afiliadoService;
public AfiliadoServiceTest()
{
_afiliadoService = new AfiliadoService(
_mapper.Object,
_notification.Object);
}
[Fact]
public async Task AdicionarUsuarioComEmailQueJaExisteDeveNotificar()
{
var request = new AddAfiliadoRequestDto();
var result = await _afiliadoService.AddAsync(request);
Assert.Null(result);
_notification
.Verify(v => v.Notify(It.IsAny<string>()), Times.Once);
// Here i want verify if method IsValid was called Once, like the verification above
}
}
I tried some things but with no success.
Exemple with what i want.
var mock = new Mock<AfiliadoService>();
mock.Verify(v => v.IsValid(request, new AddAfiliadoRequestDtoValidator()), Times.Once);
var mock = new Mock<AfiliadoService>(_mapper.Object,
_notification.Object);
var result = await mock.Object.AddAsync(request);
mock.Verify(v => v.IsValid(request, new AddAfiliadoRequestDtoValidator()), Times.Once);
This last one i got the message
Message:
Moq.MockException :
Expected invocation on the mock once, but was 0 times: v => v.IsValid<AddAfiliadoRequestDto, AddAfiliadoRequestDtoValidator>(AddAfiliadoRequestDto, AddAfiliadoRequestDtoValidator)
Performed invocations:
Mock<AfiliadoService:1> (v):
BaseService.IsValid<AddAfiliadoRequestDto, AddAfiliadoRequestDtoValidator>(AddAfiliadoRequestDto, AddAfiliadoRequestDtoValidator)
What im doing wrong?
Im using .net core - 3.1
xunit - 2.4.1
Moq - 4.16.0