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

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";
}
}

Related

Circular dependency on Elsa IWorkflowLaunchpad when injecting ISignaler with custom WorkflowContexProvider

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

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;
}
}

Cannot insert two one-to-one entities using two EF repositories in one transaction (method hangs)

I have two entities bound as one-to-one via foreignkey: CreateTenantDto and SaasTenantCreateDto.
I need to use TWO repositories (_abpTenantRepository is an instance of 3rd party repository from ABP Framework) to insert those entities into DB. I am trying to use ABP UnitOfWork implementation for this. After SaasTenantCreateDto entity is inserted, I am trying to insert CreateTenantDto entry which depends on it. If I use OnCompleted event to insert a CreateTenantDto record - the method does not enter OnCompleted before returning newTenantDto and the latter is returned as a null (the records are inserted finally, but I want to return the inserted entity if it's inserted successfully). If I don't use OnCompleted at all - the method hangs (looks like DB lock). If I use two nested UnitOfWork objects - the method hangs as well. If I use the scope for working with two repositories -
using (var scope = ServiceProvider.CreateScope())
{
var unitOfWorkManager = scope.ServiceProvider.GetRequiredService<IUnitOfWorkManager>();
using (var tenantUow = unitOfWorkManager.Begin(new AbpUnitOfWorkOptions { IsTransactional = true }))
{ ... }
}
it hangs also... It is definitely the lock and it has to do with accessing the id from the newly created newAbpTenant: I can see that in SQL Developer Sessions
enq: TX - row lock contention
and guilty session is another my HttpApi host session. Probably, the reason is as Oracle doc says: "INSERT, UPDATE, and DELETE statements on the child table do not acquire any locks on the parent table, although INSERT and UPDATE statements wait for a row-lock on the index of the parent table to clear." - SaveChangesAsync causes new record row lock?
How to resolve this issue?
//OnModelCreatingBinding
builder.Entity<Tenant>()
.HasOne(x => x.AbpTenant)
.WithOne()
.HasPrincipalKey<Volo.Saas.Tenant>(x => x.Id)
.HasForeignKey<Tenant>(x => x.AbpId);
...
b.Property(x => x.AbpId).HasColumnName("C_ABP_TENANT").IsRequired();
//Mapping ignoration to avoid problems with 'bound' entities, since using separate repositories for Insert / Update
CreateMap<CreateTenantDto, Tenant>().ForMember(x => x.AbpTenant, opt => opt.Ignore());
CreateMap<UpdateTenantDto, Tenant>().ForMember(x => x.AbpTenant, opt => opt.Ignore());
public class CreateTenantDto
{
[Required]
public int Id { get; set; }
...
public Guid? AbpId { get; set; }
public SaasTenantCreateDto AbpTenant { get; set; }
}
public async Task<TenantDto> CreateAsync(CreateTenantDto input)
{
try
{
TenantDto newTenantDto = null;
using (var uow = _unitOfWorkManager.Begin(new AbpUnitOfWorkOptions { IsTransactional = true, IsolationLevel = System.Data.IsolationLevel.Serializable }))
{
var abpTenant = await _abpTenantManager.CreateAsync(input.AbpTenant.Name, input.AbpTenant.EditionId);
input.AbpTenant.MapExtraPropertiesTo(abpTenant);
var newAbpTenant = await _abpTenantRepository.InsertAsync(abpTenant);
await uow.SaveChangesAsync();
var tenant = ObjectMapper.Map<CreateTenantDto, Tenant>(input);
tenant.AbpId = newAbpTenant.Id;
var newTenant = await _tenantRepository.InsertAsync(tenant);
newTenantDto = ObjectMapper.Map<Tenant, TenantDto>(newTenant);
await uow.CompleteAsync();
}
return newTenantDto;
}
//Implementation by ABP Framework
public virtual async Task CompleteAsync(CancellationToken cancellationToken = default)
{
if (_isRolledback)
{
return;
}
PreventMultipleComplete();
try
{
_isCompleting = true;
await SaveChangesAsync(cancellationToken);
await CommitTransactionsAsync();
IsCompleted = true;
await OnCompletedAsync();
}
catch (Exception ex)
{
_exception = ex;
throw;
}
}
I have finally resolved the problem using the following approach (but it is not using TWO repositories which seems to be impossible to implement, since we need to manipulate DbContext directly):
Application service layer:
//requiresNew: true - to be able to use TransactionScope
//isTransactional: false, otherwise it won't be possible to use TransactionScope, since we would have active ambient transaction
using var uow = _unitOfWorkManager.Begin(requiresNew: true);
var abpTenant = await _abpTenantManager.CreateAsync(input.AbpTenant.Name, input.AbpTenant.EditionId);
input.AbpTenant.MapExtraPropertiesTo(abpTenant);
var tenant = ObjectMapper.Map<CreateTenantDto, Tenant>(input);
var newTenant = await _tenantRepository.InsertAsync(tenant, abpTenant);
await uow.CompleteAsync();
return ObjectMapper.Map<Tenant, TenantDto>(newTenant);
Handmade InsertAsync method on Repository (EntityFrameworkCore) layer:
using (new TransactionScope(asyncFlowOption: TransactionScopeAsyncFlowOption.Enabled))
{
var newAbpTenant = DbContext.AbpTenants.Add(abpTenant).Entity;
tenant.AbpId = newAbpTenant.Id;
var newTenant = DbContext.Tenants.Add(tenant).Entity;
if (autoSave)
{
await DbContext.SaveChangesAsync(GetCancellationToken(cancellationToken));
}
return newTenant;
}

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

Creating webhook-notifications in testing environment

I'm currently trying to create a test webhook-notification as it's shown in the documentation:
HashMap<String, String> sampleNotification = gateway.webhookTesting().sampleNotification(
WebhookNotification.Kind.SUBSCRIPTION_WENT_PAST_DUE, "my_id"
);
WebhookNotification webhookNotification = gateway.webhookNotification().parse(
sampleNotification.get("bt_signature"),
sampleNotification.get("bt_payload")
);
webhookNotification.getSubscription().getId();
// "my_id"
First off I don't know what my_id actually should be. Is it supposed to be a plan ID? Or should it be a Subscription ID?
I've tested all of it. I've set it to an existing billing plan in my vault and I also tried to create a Customer down to an actual Subscription like this:
public class WebhookChargedSuccessfullyLocal {
private final static BraintreeGateway BT;
static {
String btConfig = "C:\\workspaces\\mz\\mz-server\\mz-web-server\\src\\main\\assembly\\dev\\braintree.properties";
Braintree.initialize(btConfig);
BT = Braintree.instance();
}
public static void main(String[] args) {
WebhookChargedSuccessfullyLocal webhookChargedSuccessfullyLocal = new WebhookChargedSuccessfullyLocal();
webhookChargedSuccessfullyLocal.post();
}
/**
*
*/
public void post() {
CustomerRequest customerRequest = new CustomerRequest()
.firstName("Testuser")
.lastName("Tester");
Result<Customer> createUserResult = BT.customer().create(customerRequest);
if(createUserResult.isSuccess() == false) {
System.err.println("Could not create customer");
System.exit(1);
}
Customer customer = createUserResult.getTarget();
PaymentMethodRequest paymentMethodRequest = new PaymentMethodRequest()
.customerId(customer.getId())
.paymentMethodNonce("fake-valid-visa-nonce");
Result<? extends PaymentMethod> createPaymentMethodResult = BT.paymentMethod().create(paymentMethodRequest);
if(createPaymentMethodResult.isSuccess() == false) {
System.err.println("Could not create payment method");
System.exit(1);
}
if(!(createPaymentMethodResult.getTarget() instanceof CreditCard)) {
System.err.println("Unexpected error. Result is not a credit card.");
System.exit(1);
}
CreditCard creditCard = (CreditCard) createPaymentMethodResult.getTarget();
SubscriptionRequest subscriptionRequest = new SubscriptionRequest()
.paymentMethodToken(creditCard.getToken())
.planId("mmb2");
Result<Subscription> createSubscriptionResult = BT.subscription().create(subscriptionRequest);
if(createSubscriptionResult.isSuccess() == false) {
System.err.println("Could not create subscription");
System.exit(1);
}
Subscription subscription = createSubscriptionResult.getTarget();
HashMap<String, String> sampleNotification = BT.webhookTesting()
.sampleNotification(WebhookNotification.Kind.SUBSCRIPTION_CHARGED_SUCCESSFULLY, subscription.getId());
WebhookNotification webhookNotification = BT.webhookNotification()
.parse(
sampleNotification.get("bt_signature"),
sampleNotification.get("bt_payload")
);
System.out.println(webhookNotification.getSubscription().getId());
}
}
but all I'm getting is a WebhookNotification instance that has nothing set. Only its ID and the timestamp appears to be set but that's it.
What I expected:
I expected to receive a Subscription object that tells me which customer has subscribed to it as well as e.g. all add-ons which are included in the billing plan.
Is there a way to get such test-notifications in the sandbox mode?
Full disclosure: I work at Braintree. If you have any further questions, feel free to contact support.
webhookNotification.getSubscription().getId(); will return the ID of the subscription associated with sampleNotification, which can be anything for testing purposes, but will be a SubscriptionID in a production environment.
Receiving a dummy object from webhookTesting().sampleNotification() is the expected behavior, and is in place to help you ensure that all kinds of webhooks can be correctly caught. Once that logic is in place, in the Sandbox Gateway under Settings > Webhooks you can specify your endpoint to receive real webhook notifications.
In the case of SUBSCRIPTION_CHARGED_SUCCESSFULLY you will indeed receive a Subscription object containing add-on information as well as an array of Transaction objects containing customer information.

Resources