Circular dependency on Elsa IWorkflowLaunchpad when injecting ISignaler with custom WorkflowContexProvider - elsa-workflows

I'm evaluating Elsa for a new project at work, but have run into trouble when creating a service that DIs ISignaler, only when I also have a WorkflowContextProvider as indicated below.
My WorkflowProvider:
public class ContractorWorkflowContextProvider : WorkflowContextRefresher<ContractorRecruitVM>
{
private IContractorApi _contractorApi;
public ContractorWorkflowContextProvider(IContractorApi api)
{
_contractorApi = api;
}
public override async ValueTask<ContractorRecruitVM?> LoadAsync(LoadWorkflowContext context, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(context.ContextId))
{
return null;
}
var ctrId = int.Parse(context.ContextId);
var vm = await _contractorApi.GetById(ctrId);
return vm;
}
...
}
ContractorApi:
public class ContractorApi : IContractorApi
{
IPersonRepo _personRepo;
IElsaDemoUOW _uow;
IElsaSignalService _elsaSignalService;
public ContractorApi(IPersonRepo personRepo, IElsaDemoUOW uow, IElsaSignalService elsaSignalService)
{
_personRepo = personRepo;
_uow = uow;
_elsaSignalService = elsaSignalService;
}
}
and my ElsaSignalService:
public class ElsaSignalService : IElsaSignalService
{
ISignaler _signaler;
public ElsaSignalService(ISignaler signaler)
{
_signaler = signaler;
}
public async Task<bool> CtrInitDocsUploaded(string workflowInstanceId)
{
var res = await _signaler.TriggerSignalAsync(signal: "ctr-init-docs-uploaded", workflowInstanceId: workflowInstanceId);
return true;
}
}
DI is setup like:
...
services
.AddElsa(elsa => elsa
.UseEntityFrameworkPersistence(ef => ef.UseSqlServer(elsaConnString, b => b.MigrationsAssembly("Elsa.Persistence.EntityFramework.SqlServer")))
.AddConsoleActivities()
.AddHttpActivities(elsaSection.GetSection("Server").Bind)
.AddQuartzTemporalActivities()
.AddJavaScriptActivities()
.AddWorkflowsFrom<Startup>()
.AddEmailActivities(elsaSection.GetSection("Smtp").Bind)
)
.AddWorkflowContextProvider<ContractorWorkflowContextProvider>();
...
services.AddTransient<IElsaDemoUOW, ElsaDemoUOW>();
services.AddTransient<IPersonRepo, PersonRepo>();
services.AddTransient<IContractorApi, ContractorApi>();
services.AddTransient<IElsaSignalService, ElsaSignalService>();
...
I'm not sure if I'm going about this correctly at all, but my goal is to have my "ElsaSignalService" that I'll be calling from the API, and that ElsaSignalService will raise whatever signal is appropriate.
Elsa will receive the signals, and in some/many cases save the workflowcontext back (using the custom provider I specified). If this is a valid setup, I'm not sure where the circular reference error is coming from, as I don't see one. If I'm misunderstanding how to structure Elsa to do what I'm after, would greatly appreciate input.
When I hit the app with the setup described above, I get the following circular dependency error but nothing I see looks like it's creating one.
A circular dependency was detected for the service of type 'Elsa.Services.IWorkflowLaunchpad'.
Elsa.Services.IWorkflowLaunchpad(Elsa.Services.Workflows.WorkflowLaunchpad) ->
Elsa.Services.IWorkflowRunner(Elsa.Services.Workflows.WorkflowRunner) ->
Elsa.Services.IWorkflowContextManager(Elsa.Services.WorkflowContexts.WorkflowContextManager) ->
System.Collections.Generic.IEnumerable<Elsa.Providers.WorkflowContexts.IWorkflowContextProvider> ->
Elsa.Providers.WorkflowContexts.IWorkflowContextProvider(ElsaDemo.DemoApp.WorkflowContexts.ContractorWorkflowContextProvider) ->
ElsaDemo.DemoApp.API.IContractorApi(ElsaDemo.DemoApp.API.ContractorApi) ->
ElsaDemo.DemoApp.Services.IElsaSignalService(ElsaDemo.DemoApp.Services.ElsaSignalService) ->
Elsa.Activities.Signaling.Services.ISignaler(Elsa.Activities.Signaling.Services.Signaler) ->
Elsa.Services.IWorkflowLaunchpad

Related

Entity Framework Core CompileAsyncQuery syntax to do a query returning a list?

Documentation and examples online about compiled async queries are kinda sparse, so I might as well ask for guidance here.
Let's say I have a repository pattern method like this to query all entries in a table:
public async Task<List<ProgramSchedule>> GetAllProgramsScheduledList()
{
using (var context = new MyDataContext(_dbOptions))
{
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
return await context.ProgramsScheduledLists.ToListAsync();
}
}
This works fine.
Now I want to do the same, but with an async compiled query.
One way I managed to get it to compile is with this syntax:
static readonly Func<MyDataContext, Task<List<ProgramSchedule>>> GetAllProgramsScheduledListQuery;
static ProgramsScheduledListRepository()
{
GetAllProgramsScheduledListQuery = EF.CompileAsyncQuery<MyDataContext, List<ProgramSchedule>>(t => t.ProgramsScheduledLists.ToList());
}
public async Task<List<ProgramSchedule>> GetAllProgramsScheduledList()
{
using (var context = new MyDataContext(_dbOptions))
{
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
return await GetAllProgramsScheduledListQuery(context);
}
}
But then on runtime this exception get thrown:
System.ArgumentException: Expression of type 'System.Collections.Generic.List`1[Model.Scheduling.ProgramSchedule]' cannot be used for return type 'System.Threading.Tasks.Task`1[System.Collections.Generic.List`1[Model.Scheduling.ProgramSchedule]]'
The weird part is that if I use any other operator (for example SingleOrDefault), it works fine. It only have problem returning List.
Why?
EF.CompileAsync for set of records, returns IAsyncEnumrable<T>. To get List from such query you have to enumerate IAsyncEnumrable and fill List,
private static Func<MyDataContext, IAsyncEnumerable<ProgramSchedule>> compiledQuery =
EF.CompileAsyncQuery((MyDataContext ctx) =>
ctx.ProgramsScheduledLists);
public static async Task<List<ProgramSchedule>> GetAllProgramsScheduledList(CancellationToken ct = default)
{
using (var context = new MyDataContext(_dbOptions))
{
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var result = new List<ProgramSchedule>();
await foreach (var s in compiledQuery(context).WithCancellation(ct))
{
result.Add(s);
}
return result;
}
}

call private method (PowerMockito Test)

First a save() method is executed which passes the test until it reaches a condition, where if it is true it calls the saveBankAccountAndRole() method and if it is false it sends a Mono.error(new Exception("...").
The sizeAccounts(String customerId) method does pass the test.
In the saveBankAccountAndRole(BankAccountDto bnkDto) method, after executing the sizeAccounts() method, the test does not pass, what am I missing?
public Flux<BankAccountDto> findAccountsByCustomerId(String customerId) {
return mongoRepository
.findAll()
.filter(ba ->
ba.getCustomerId().equals(customerId))
.map(AppUtils::entityToDto);
}
private Mono<Long> sizeAccounts(String customerId){
return findAccountsByCustomerId(customerId)
.count();
}
private Mono<BankAccountDto> saveBankAccountAndRole(BankAccountDto bnkDto) {
return sizeAccounts(bnkDto.getCustomerId())
.flatMap(number -> {
bnkDto.setOpeningOrder(number + 1);
return mongoRepository.save(AppUtils.dtoToEntity(bnkDto))
.switchIfEmpty(Mono.error(new Exception("Problem saving the bank account")))
.zipWhen(bnk -> {
var customerRoleDto = new CustomerRoleDto();
customerRoleDto.setBankAccountId(bnk.getBankAccountId());
customerRoleDto.setCustomerId(bnkDto.getCustomerId());
customerRoleDto.setRoleType(bnkDto.getRoleType());
return webClientRoleHelper.saveCustomerRole(customerRoleDto);
})
.switchIfEmpty(Mono.error(new Exception("Problem saving roles")))
.map(tuple -> AppUtils.entityToDto(tuple.getT1()));
});
}
test:
#Mock
private IMongoBankAccountRepository mongoRepository;
#InjectMocks
private BankAccountServiceImpl bankAccountServiceImpl;
#Test
void saveBankAccountAndRoleTest() throws Exception {
when(mongoRepository.findAll()).thenReturn(Flux.just(bnkDto)
.map(AppUtils::dtoToEntity));
when(mongoRepository.findAll().filter(ba ->
ba.getCustomerId().equals(customerId)))
.thenReturn(Flux.just(bnkDto).map(AppUtils::dtoToEntity));
StepVerifier.create(bankAccountServiceImpl.findAccountsByCustomerId(customerId))
.expectSubscription()
.expectNext(bnkDto)
.verifyComplete();
var spy = PowerMockito.spy(bankAccountServiceImpl);
PowerMockito.when(spy, "sizeAccounts", customerId)
.thenReturn(Mono.just(2L));
PowerMockito.when(spy, "saveBankAccountAndRole",bnkDto)
.thenReturn(Mono.just(bnkDto));
}
exception:
java.lang.AssertionError: expectation "expectNext(com.nttdata.bootcamp.model.dto.BankAccountDto#147c4523)" failed (expected value: com.nttdata.bootcamp.model.dto.BankAccountDto#147c4523; actual value: com.nttdata.bootcamp.model.dto.BankAccountDto#551725e4) at com.nttdata.bootcamp.business.impl.BankAccountServiceImplTest.saveBankAccountAndRoleTest(BankAccountServiceImplTest.java:267)
Which sends me when verifyComplete()
By looking at the code in your test, you shouldn't expect an specific object to be returned.
when(mongoRepository.findAll()).thenReturn(Flux.just(bnkDto)
.map(AppUtils::dtoToEntity));
when(mongoRepository.findAll().filter(ba ->
ba.getCustomerId().equals(customerId)))
.thenReturn(Flux.just(bnkDto).map(AppUtils::dtoToEntity));
The code above is mapping that DTO object to an Entity, which makes sense for a repository. However, that means that the following piece of code will "remap" it to a newly created object:
.zipWhen(bnk -> {
var customerRoleDto = new CustomerRoleDto();
customerRoleDto.setBankAccountId(bnk.getBankAccountId());
customerRoleDto.setCustomerId(bnkDto.getCustomerId());
customerRoleDto.setRoleType(bnkDto.getRoleType());
return webClientRoleHelper.saveCustomerRole(customerRoleDto);
})
Thus, you should be expecting an object with that same class, containing the same instance's variables values. But you can't expect it to be the exact same object.
You might want to try this (untested code):
StepVerifier.create(bankAccountServiceImpl.findAccountsByCustomerId(customerId))
.expectSubscription()
.expectMatches(dto -> dto.getBankAccountId().equals(bankDto.getBankAccountId) && dto.getCustomerId.equals(bnkDto.getCustomerId))
.verifyComplete();
I hope that works out for you.

How to test a state stored aggregate that doesn't produce events

I want to test a state stored aggregate by using AggregateTestFixture. However I get AggregateNotFoundException: No 'given' events were configured for this aggregate, nor have any events been stored. error.
I change the state of the aggregate in command handlers and apply no events since I don't want my domain entry table to grow unnecessarily.
Here is my external command handler for the aggregate;
open class AllocationCommandHandler constructor(
private val repository: Repository<Allocation>,
) {
#CommandHandler
fun on(cmd: CreateAllocation) {
this.repository.newInstance {
Allocation(
cmd.allocationId
)
}
}
#CommandHandler
fun on(cmd: CompleteAllocation) {
this.load(cmd.allocationId).invoke { it.complete() }
}
private fun load(allocationId: AllocationId): Aggregate<Allocation> =
repository.load(allocationId)
}
Here is the aggregate;
#Entity
#Aggregate
#Revision("1.0")
final class Allocation constructor() {
#AggregateIdentifier
#Id
lateinit var allocationId: AllocationId
private set
var status: AllocationStatusEnum = AllocationStatusEnum.IN_PROGRESS
private set
constructor(
allocationId: AllocationId,
) : this() {
this.allocationId = allocationId
this.status = AllocationStatusEnum.IN_PROGRESS
}
fun complete() {
if (this.status != AllocationStatusEnum.IN_PROGRESS) {
throw IllegalArgumentException("cannot complete if not in progress")
}
this.status = AllocationStatusEnum.COMPLETED
apply(
AllocationCompleted(
this.allocationId
)
)
}
}
There is no event handler for AllocationCompleted event in this aggregate, since it is listened by an other aggregate.
So here is the test code;
class AllocationTest {
private lateinit var fixture: AggregateTestFixture<Allocation>
#Before
fun setUp() {
fixture = AggregateTestFixture(Allocation::class.java).apply {
registerAnnotatedCommandHandler(AllocationCommandHandler(repository))
}
}
#Test
fun `create allocation`() {
fixture.givenNoPriorActivity()
.`when`(CreateAllocation("1")
.expectSuccessfulHandlerExecution()
.expectState {
assertTrue(it.allocationId == "1")
};
}
#Test
fun `complete allocation`() {
fixture.givenState { Allocation("1"}
.`when`(CompleteAllocation("1"))
.expectSuccessfulHandlerExecution()
.expectState {
assertTrue(it.status == AllocationStatusEnum.COMPLETED)
};
}
}
create allocation tests passes, I get the error on complete allocation test.
The givenNoPriorActivity is actually not intended to be used with State-Stored aggregates. Quite recently an adjustment has been made to the AggregateTestFixture to support this, but that will be released with Axon 4.6.0 (the current most recent version is 4.5.1).
That however does not change the fact I find it odd the complete allocation test fails. Using the givenState and expectState methods is the way to go. Maybe the Kotlin/Java combination is acting up right now; have you tried doing the same with pure Java, just for certainty?
On any note, the exception you share comes from the RecordingEventStore inside the AggregateTestFixture. It should only occur if an Event Sourcing Repository is used under the hood (by the fixture) actually since that will read events. What might be the culprit, is the usage of the givenNoPriorActivity. Please try to replace that for a givenState() providing an empty Aggregate instance.

Masstransit Testharness with multiple ExecuteActivities doesn't create Endpoints as expected

I want to write a test that checks if my routingslip works as expected. I narrowed it down to this simplified Version.
namespace MasstransitTest
{
public class Tests
{
private readonly InMemoryTestHarness _harness;
public Tests()
{
var services = new ServiceCollection();
services.AddLogging(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Debug));
services.AddMassTransitInMemoryTestHarness(cfg =>
{
cfg.AddExecuteActivity<ActivityOne, MyMessage>()
.Endpoint(c => c.Name = "queue1");
cfg.AddExecuteActivity<ActivityTwo, MyMessage>()
.Endpoint(c => c.Name = "queue2");
});
var serviceProvider = services.BuildServiceProvider(true);
_harness = serviceProvider.GetRequiredService<InMemoryTestHarness>();
_harness.Start();
}
[Test]
public async Task Test1()
{
var routingSlipBuilder = new RoutingSlipBuilder(Guid.NewGuid());
routingSlipBuilder.AddActivity("Activity1", new Uri("loopback://localhost/queue1"), new { MyMessage = new MyMessage()});
routingSlipBuilder.AddActivity("Activity2", new Uri("loopback://localhost/queue2"), new { MyMessage = new MyMessage()});
routingSlipBuilder.AddSubscription(new Uri("loopback://localhost/protocol-event-monitor"),RoutingSlipEvents.All, RoutingSlipEventContents.All);
var routingSlip = routingSlipBuilder.Build();
await _harness.Bus.Execute(routingSlip);
Assert.That(await _harness.Sent.Any<RoutingSlipCompleted>());
}
}
}
This Test failes, but it works if I replace one of the activities by an activity with another argument type. For example
cfg.AddExecuteActivity<ActivityTwo, MyOtherMessage>().Endpoint(c => c.Name = "queue2");
The failing test prints this log:
info: MassTransit[0] Configured endpoint queue2, Execute Activity: MasstransitTest.ActivityOne
info: MassTransit[0] Configured endpoint queue2, Execute Activity: MasstransitTest.ActivityTwo
dbug: MassTransit[0] Starting bus: loopback://localhost/
I think the Problem is that only one endpoint gets configured, but I don't know why. Is this a bug in the Testingframework?
When using .Endpoint to override the execute or compensate endpoint for an activity, the arguments or log type must be unique.
To change the endpoint name for activities that have a common argument or log type, use an ActivityDefinition or an ExecuteActivityDefinition
public class ActivityOnExecuteActivityDefinition :
ExecuteActivityDefinition<ActivityOne, One>
{
public ActivityOnExecuteActivityDefinition()
{
EndpointName = "queue1";
}
}

ObjectDisposedException during tests

READ EDIT
I have a similar implementation to AsyncCrudAppService related to filtering queries. When I run tests on top of ABPs implementation of Application Services derived of AsyncCrudAppServiceBase, everything runs fine. When I do the same running on top of my custom "filters", I get the following error:
System.ObjectDisposedException : Cannot access a disposed object [...]
Object name: 'DataManagerDbContext'.
I know the solution is using IUnitOfWorkManager and calling Begin() method to define a UnitOfWork, but since I am working with AppServices, I thought there was already a UnitOfWork defined. These are my methods:
public PagedResultDto<StateDetails> GetEditorList(EditorRequestDto input)
{
var query = _stateRepository.GetAllIncluding(p => p.Country).AsQueryable();
query = ApplySupervisorFilter(query);
query = query.ApplyFiltering(input, "Name");
var totalCount = query.Count();
query = query.ApplySorting<State, int, PagedAndSortedResultRequestDto>(input);
query = query.ApplyPaging<State, int, PagedAndSortedResultRequestDto>(input);
var entities = query.ToList();
return new PagedResultDto<StateDetails>(totalCount, ObjectMapper.Map<List<StateDetails>>(entities));
}
private IQueryable<State> ApplySupervisorFilter(IQueryable<State> query)
{
if (!SettingManager.GetSettingValue<bool>(AppSettingNames.SupervisorFlag))
{
query = ApplyUncategorizedFilter(query);
}
return query;
}
private IQueryable<State> ApplyUncategorizedFilter(IQueryable<State> query)
{
return query.Where(
p => !p.CountryId.HasValue);
}
My passing test (with manual UnitOfWork):
[Fact]
public async Task GetEditorListWithouSupervisorFlag_Test()
{
using (UnitOfWorkManager.Begin())
{
await ChangeSupervisorFlag(false);
var result = _stateAppService.GetEditorList(
new EditorRequestDto
{
MaxResultCount = 10,
});
result.Items.Any(p => p.Country == null).ShouldBe(true);
}
}
Does anybody know an solution to this "issue"? It would be annoying to define a UnitOfWork for every test I perform. It also seems like I am doing something wrong
EDIT
I have solved the issue. I must use an interface for my Application Service when running tests so it is able to mock it properly
I have solved the issue. I must use an interface for my Application Service when running tests so it is able to mock it properly

Resources