I try to use saga with MassTransit 7.2.2.
I have next event in my saga:
During(Submitted,
When(OrderAccepted)
.Then(x =>
{
logger.LogInformation($"Order {x.Instance.OrderId} accepted");
throw new Exception("TEST");
})
.Catch<Exception>(x =>
{
x.If(
context => context.Data.OrderId == 1002,
activityBinder =>
activityBinder
.Then(y =>
{
logger.LogInformation($"Order {y.Instance.OrderId} catch exception and pass to Rejected");
})
.TransitionTo(Rejected)
);
return x;
})
.ThenAsync(c =>
{
return TakeProductCommand(c);
})
.TransitionTo(Accepted));
In code above I want to catch any exception that might be raised during handling of current event and put saga into Rejected state. But it doesn't work as I thought. I don't get into the Catch handler at all.
What I did wrong?
Your code is slightly malformed, and breaks the builder pattern chain.
The changes are subtle, but significant.
During(Submitted,
When(OrderAccepted)
.Then(x =>
{
logger.LogInformation($"Order {x.Instance.OrderId} accepted");
throw new Exception("TEST");
})
.Catch<Exception>(x =>
x.If(
context => context.Data.OrderId == 1002,
activityBinder =>
activityBinder
.Then(y =>
{
logger.LogInformation($"Order {y.Instance.OrderId} catch exception and pass to Rejected");
})
.TransitionTo(Rejected)
)
)
.ThenAsync(c =>
{
return TakeProductCommand(c);
})
.TransitionTo(Accepted));
Related
I currently register Masstransit for two bus controls. I would like to inject ISendEndPointProvider for SecondBus as DI (I don't want to call GetSendEndPoint() in caller). How can I do that. When ISendEndPointProvider always return for the first bus.
Program.cs
services.AddMassTransit(x =>
{
x.AddConsumer<FirstConsumer>();
x.AddBus(provider => Bus.Factory.CreateUsingRabbitMq(cfg =>
{
cfg.Host(new Uri(rabbitMqOption.RabbitMqHost), h =>
{
h.Username(rabbitMqOption.RabbitMqUser);
h.Password(rabbitMqOption.RabbitMqPassword);
h.Heartbeat(10);
});
cfg.ReceiveEndpoint(rabbitMqOption.RabbitMqQueue, e =>
{
e.PrefetchCount = rabbitMqOption.RabbitMqPrefetchCount;
e.Consumer<FirstConsumer>(provider.Container);
});
}));
});
services.AddMassTransit<ISecondBus, SecondBus>(x =>
{
x.AddBus(provider => Bus.Factory.CreateUsingRabbitMq(cfg =>
{
cfg.Host(rabbitMqOption.NotificationRabbitMqHost, hostConfig =>
{
hostConfig.Username(rabbitMqOption.RabbitMqUser);
hostConfig.Password(rabbitMqOption.RabbitMqPassword);
});
}));
});
When using MultiBus, the ISendEndpointProvider and IPublishEndpoint interfaces when outside of a consumer will always point to IBus. There is no way to get the second bus without depending upon ISecondBus.
If the interface is used by a consumer (or a scoped consumer dependency), those interfaces will refer to the bus on which the message was received.
Configuration Errors
Also, you should change your .AddBus calls to .UsingRabbitMq((context, cfg), which gives you proper access to the container via the context argument. The AddBus syntax will eventually be deprecated.
The correct syntax is shown below (along with a corrected consumer configuration):
services.AddMassTransit(x =>
{
x.AddConsumer<FirstConsumer>();
x.UsingRabbitMq((context, cfg) =>
{
cfg.Host(new Uri(rabbitMqOption.RabbitMqHost), h =>
{
h.Username(rabbitMqOption.RabbitMqUser);
h.Password(rabbitMqOption.RabbitMqPassword);
h.Heartbeat(10);
});
cfg.ReceiveEndpoint(rabbitMqOption.RabbitMqQueue, e =>
{
e.PrefetchCount = rabbitMqOption.RabbitMqPrefetchCount;
e.ConfigureConsumer<FirstConsumer>(context);
});
}));
});
Adding a bit more for a solution that worked on my end after a couple hours of trial and error.
First, add this Nuget package: MassTransit.Extensions.DependencyInjection,
Then, here is exactly how I'm registering MassTransit:
services.AddMassTransit(x =>
{
x.AddConsumer(typeof(MyConsumerType));
x.UsingAzureServiceBus((context, cfg) =>
{
cfg.Host(configuration[Constants.KeyVaultSecretServiceBusConnectionString]);
cfg.Message<IDataPrivacyEvent>(cfg => cfg.SetEntityName(MessagingConstants.DataPrivacyEndpoint));
cfg.ConfigureEndpoints(context);
cfg.UseMessageRetry(m => m.Interval(5, TimeSpan.FromSeconds(10)));
cfg.SubscriptionEndpoint<IValidationEvent>(MessagingConstants.ValidationEndpoint, e =>
{
e.ConfigureConsumer(context, typeof(MyConsumerType));
e.UseMessageRetry(m => m.Interval(5, TimeSpan.FromSeconds(10)));
});
});
});
I have to make 5 requests (order doesn't matter) to 5 different endpoints. The URL of these endpoints is the same, except for the business line. These business lines are the array of the from.
I want show a skeleton loader before each request and hide once it finish. So, basically the flow is:
1. [Hook - before request]
2. [Log of data fetched]
3. [Hook - after request]
This is my service:
export function getInsurances(
userIdentity: string,
hooks?: RequestHooks
): Observable<Policy[]> {
return from(["all", "vehicle", "health", "soat", "plans"]).pipe(
tap(() => hooks?.beforeRequest && hooks.beforeRequest()),
flatMap<string, Observable<Policy[]>>(businessLine => {
return InsurancesApi.getPolicies<Policy>(
userIdentity,
businessLine
).pipe(
map(policies => {
return policies.map(policy => PolicyStandarizer(policy));
}),
finalize(() => {
hooks?.afterRequest && hooks.afterRequest();
})
);
}),
catchError(err => of(err)),
takeUntil(HttpCancelator)
);
}
This is my subscribe:
const hooks = {
beforeRequest() {
Log.info("Before Request");
setStatus(HttpRequestStatus.PENDING);
},
afterRequest() {
Log.warn("After Request");
setStatus(HttpRequestStatus.RESOLVED);
},
};
getInsurances(userIdentity, hooks).subscribe(
policies => {
Log.normal("Policies:", policies);
setInsurances(policies);
},
(err: Error) => {
setError(err);
}
);
And have this output (sorry for paste the link, I can't embed the image because rep):
https://i.stack.imgur.com/Nbq49.png
The finalize is working fine, but the tap is executing five times at once.
Thank you.
I think you get this behavior because from emits the items synchronously, so its essentially the same as doing:
for (let i = 0; i < arr.length; i++) {
console.log('before req');
observer.next(arr[i]);
}
observer.complete();
afterRequest is shown properly because the actions involved are asynchronous.
If you want to trigger that event only once, before all the requests are fired, you could try this:
from([...])
.pipe(
finalize(() => hooks?.beforeRequest && hooks.beforeRequest()),
flatMap(/* ... */)
)
EDIT - log event before each request
flatMap(
value => concat(
of(null).pipe(
tap(() => hooks?.beforeRequest && hooks.beforeRequest()),
ignoreElements(), // Not interested in this observable's values
),
InsurancesApi.getPolicies(/* ... */)
)
)
I found some strange behavior on rxjs and I want to know if it's the expected behavior or if I missed something (I use angular but I thing it's not related) :
public someFunction() {
// this woks as expected: log "test1"
return of(null).pipe(
switchMap(() => this.errorTest()),
catchError((e: Error) => {
console.log('test1');
return throwError(e);
})
);
// "test2" is never logged
return this.errorTest().pipe(
catchError((e: Error) => {
console.log('test2');
return throwError(e);
})
);
}
private errorTest(): Observable<any> {
throw Error('one error');
}
when I subscribe someFunction the first function logs "test1", but the second doesn't log anything, error is no catched...
private errorTest(): Observable<any> {
throw Error('one error');
}
Does not return an observable.
switchMap(() => this.errorTest())
Catches errors from the callback function.
switchMap(() => throw Error('one error'))
Is the same as your example, and this.errorTest().pipe() never returns so you don't get an error for trying to call .pipe() on undefined.
What you wanted to do was this.
private errorTest(): Observable<any> {
return throwError(new Error('one error'));
}
https://rxjs-dev.firebaseapp.com/api/index/function/throwError
I have a network call where it's likely that api will throw an 400 error. I want to handle this gracefully.
Right now I do it like below -
private fetchStatus(objectId: string): Observable<string> {
return Observable.create((observer) => {
this.http.get('/api/data-one').subscribe(response => {
if (response.result === 'SUCCESS') {
observer.next('SUCCESS');
} else {
observer.next('DENIED');
}
observer.complete();
},
error => {
observer.next('DENIED');
observer.complete();
});
});
}
But I will prefer doing it with Observable.map operator. The problem with Observable.map is when api throws a 400 the entire observable goes in error mode.
I want to prevent this because this get call is being used in a forkJoin with other calls. Failure of this would mean failure of the entire forkJoin below
forkJoin([
this.http.get('/api/route-2'),
this.http.get('/api/route-1'),
this.fetchStatus('abc')
]).subscribe((responseCollection: any) => {
observer.next({
result1: responseCollection[0],
result2: responseCollection[1],
result3: responseCollection[2]
});
observer.complete();
}, error => observer.error(error));
You can do this with map and catchError.
catchError will catch any error thrown by the source and return a new Observable. This new Observable is what, in your case, will be passed to forkJoin in the case of a HTTP error.
private fetchStatus(objectId: string): Observable<string> {
return this.http.get('/api/data-one').pipe(
map(response => response.result === 'SUCCESS' ? 'SUCCESS' : 'DENIED'),
catchError(error => of('DENIED')),
);
}
How can i with cypress show an custom error message when element is not present?
For the snippet below, i would like to display: "No rows are displayed" instead of the provided; "expected #rows to exist in the DOM".
cy.get('#rows').should('exist');
Cypress event handling gives a hook that may be used to customize the error message.
The Cypress log shows errors in the format ${error.name}:${error.message}. You can change both error properties, but the : is hard-coded.
Here are some samples,
describe('custom error', () => {
// Ref: https://docs.cypress.io/api/events/catalog-of-events.html#Catching-Test-Failures
it('fails with custom error message', () => {
cy.on('fail', (error, runnable) => {
error.name = 'CustomError'
error.message = 'Incorrect, 1 !== 2'
throw error // throw error to have test still fail
})
cy.wrap(1).should('eq', 2)
})
/*
Ref: https://docs.cypress.io/api/cypress-api/custom-commands.html#Child-Commands
Add this to /cypress/support/commands.js
*/
Cypress.Commands.add('onFail', { prevSubject: true }, (chainedSubject, message) => {
cy.on('fail', (error, runnable) => {
error.name = 'CustomError'
error.message = 'Incorrect, 1 !== 2'
throw error // throw error to have test still fail
})
return chainedSubject
})
it('fails with custom message via command', () => {
cy.wrap(1).onFail(customError).should('eq', 2)
})
/*
Ref: https://docs.cypress.io/api/cypress-api/custom-commands.html#Overwrite-Existing-Commands
Add this to /cypress/support/commands.js
*/
Cypress.Commands.overwrite('should', (originalFn, actual, assertion, expected, options) => {
if (options && options.message) {
cy.on('fail', (error, runnable) => {
error.name = 'CustomError'
error.message = options.message
throw error // throw error to have test still fail
})
}
return originalFn(actual, assertion, expected, options)
})
it.only('fails with custom message via overwrite of should', () => {
cy.wrap(1).should('eq', 2, { message: 'Incorrect: 1 !== 2'})
})
it('fails with standard message', () => {
cy.wrap(1).should('eq', 2)
})
})
It also works with cy.get()
This test uses a cy.get() and it also emits a custom message
it('fails with a custom message when using cy.get()', () => {
cy.visit('https://docs.cypress.io/api/commands/get.html')
cy.get('h1').onFail('Failed to find this text').should('contain', 'NoSuchText')
})