Troubleshooting MassTransit request client when using RoutingSlipRequestProxy/RoutingSlipResponseProxy for handling courier activities - masstransit

Can someone give a working example in ASP.NET Core (with DI) of using RequestClient with RoutingSlipRequestProxy/RoutingSlipResponseProxy? I am trying the following code but somehow my message goes into skipped queue and I never get a reply in the controller action. The code in CreateESimOrderCommandResponseConsumer never gets executed:
public class CreateESimOrderCommandConsumer : RoutingSlipRequestProxy<CreateESimOrderCommand>
{
protected override Task BuildRoutingSlip(RoutingSlipBuilder builder, ConsumeContext<CreateESimOrderCommand> request)
{
builder.AddActivity("OrderSaveToDb", QueueNames.GetActivityUri(nameof(OrderSaveToDbActivity)));
builder.AddActivity("CreatePreactiveSubscriber", QueueNames.GetActivityUri(nameof(CreatePreactiveSubscriberActivity)));
builder.AddActivity("OrderUpdateStatus", QueueNames.GetActivityUri(nameof(OrderUpdateStatusActivity)));
builder.SetVariables(new
{
ProfileId = request.Message.ESimCatalogItemProfileId,
});
return Task.CompletedTask;
}
}
public class CreateESimOrderCommandResponseConsumer : RoutingSlipResponseProxy<CreateESimOrderCommand, SubscriberCreationRequested>
{
protected override Task<SubscriberCreationRequested> CreateResponseMessage(ConsumeContext<RoutingSlipCompleted> context, CreateESimOrderCommand request)
{
SubscriberCreationRequested subscriberCreationRequestedImpl = new SubscriberCreationRequestedImpl(context.GetVariable<string>("PhoneNumber"));
return Task.FromResult(subscriberCreationRequestedImpl);
}
}
public interface SubscriberCreationRequested
{
public string PhoneNumber { get; }
}
public record SubscriberCreationRequestedImpl(string PhoneNumber): SubscriberCreationRequested;
public interface CreateESimOrderCommand
{
public int ESimCatalogItemProfileId { get; }
}
In program.cs
builder.Services.AddMassTransit(x =>
{
x.SetKebabCaseEndpointNameFormatter();
x.UsingRabbitMq((context, cfg) =>
{
cfg.AutoStart = true;
cfg.ConfigureEndpoints(context, KebabCaseEndpointNameFormatter.Instance);
cfg.Host("localhost", "/", h =>
{
h.Username("guest");
h.Password("guest");
});
});
x.AddRequestClient<CreateESimOrderCommandConsumer>();
x.AddRequestClient<CreateESimOrderCommandResponseConsumer>();
x.AddConsumersFromNamespaceContaining<CreateESimOrderCommandConsumer>();
x.AddActivitiesFromNamespaceContaining<ESimOrderSaveToDbActivity>();
});
In my asp.net core controller:
private readonly IRequestClient<CreateESimOrderCommand> requestCreateOrderRequestConsumerClient;
private readonly ILogger<ESimController> logger;
public ESimController(
IRequestClient<CreateESimOrderCommand> requestCreateOrderRequestConsumerClient,
ILogger<ESimController> logger)
{
this.logger = logger;
this.requestCreateOrderRequestConsumerClient = requestCreateOrderRequestConsumerClient;
}
[HttpPost]
public async Task<IActionResult> Generate(ESimGenerateModel eSimGenerateModel, CancellationToken cancellationToken)
{
var resp = await requestCreateOrderRequestConsumerClient.GetResponse<SubscriberCreationRequested>(new
{
ESimCatalogItemProfileId = eSimGenerateModel.ESimProfileId,
}, cancellationToken);
logger.LogInformation("Resp = {0}", resp.Message.PhoneNumber);
return RedirectToAction("Index");
}
The console logging shows that the message goes to SKIP queue:
[10:31:21.994 DBG] [] SEND rabbitmq://localhost/order-update-status_execute?bind=true 0c4f0000-2019-c2f6-cdaa-08db0a78061b MassTransit.Courier.Contracts.RoutingSlip [s:MassTransit.Messages]
[10:31:21.994 DBG] [] RECEIVE rabbitmq://localhost/create-crmpreactive-subscriber_execute 0c4f0000-2019-c2f6-cdaa-08db0a78061b MassTransit.Courier.Contracts.RoutingSlip [....].ESim.CourierActivities.CreatePreactiveSubscriberActivity(00:00:00.0552592) [s:MassTransit.Messages]
[10:31:22.137 DBG] [] SKIP rabbitmq://localhost/create-esim-order-command 0c4f0000-2019-c2f6-0750-08db0a780650 [s:MassTransit.Messages]
[10:31:22.140 DBG] [] SEND rabbitmq://localhost/create-esim-order-command 0c4f0000-2019-c2f6-0750-08db0a780650 MassTransit.Courier.Contracts.RoutingSlipCompleted [s:MassTransit.Messages]
[10:31:22.140 DBG] [] RECEIVE rabbitmq://localhost/order-update-status_execute 0c4f0000-2019-c2f6-cdaa-08db0a78061b MassTransit.Courier.Contracts.RoutingSlip [...].ESim.CourierActivities.OrderUpdateStatusActivity(00:00:00.1486087) [s:MassTransit.Messages]
For calling the request/response proxy consumer from a saga I came up with the following code:
public class ESimOrderStateMachine : MassTransitStateMachine<ESimOrderState>
{
static ESimOrderStateMachine()
{
MessageContracts.Initialize();
}
public State ESimOrderSubscriberPendingActivation { get; set; }
public Event<ESimCreateOrder> ESimOrderSubmittedEvent { get; set; }
public ESimOrderStateMachine(ILogger<ESimOrderStateMachine> logger)
{
Request(() => CreateCRMSubscriber);
InstanceState(m => m.CurrentState);
Event(() => ESimOrderSubmittedEvent);
Initially(
When(ESimOrderSubmittedEvent)
.Then(context =>
{
context.Saga.CorrelationId = context.Message.CorrelationId;
})
.Then(x => logger.LogInformation("ESim order submitted"))
.Request(CreateCRMSubscriber, context => context.Init<CreateESimOrderCommand>(new
{
ESimCatalogItemProfileId = context.Message.ESimCatalogItemProfileId,
}))
.TransitionTo(ESimOrderSubscriberPendingActivation)
);
During(ESimOrderSubscriberPendingActivation,
When(ESimOrderCancelRequestEvent)
.Finalize()
);
}
public Request<ESimOrderState, CreateESimOrderCommand, SubscriberCreationRequested> CreateCRMSubscriber { get; set; }
}
Not sure if this is the intended way to make a request using request/response proxies from saga, but when I execute the saga, I get a fault:
MassTransit.EventExecutionException: The ESimOrderSubmittedEvent<ESimCreateOrder> (Event) execution faulted
---> MassTransit.EventExecutionException: The ESimOrderSubmittedEvent<ESimCreateOrder> (Event) execution faulted
---> MassTransit.EventExecutionException: The ESimOrderSubmittedEvent<ESimCreateOrder> (Event) execution faulted
---> MassTransit.ConfigurationException: A request timeout was specified but no message scheduler was specified or available
at MassTransit.SagaStateMachine.RequestActivityImpl`3.SendRequest(BehaviorContext`1 context, SendTuple`1 sendTuple, Uri serviceAddress) in /_/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/RequestActivityImpl.cs:line 45
[...]

Both proxy consumers, the request and the response proxy, should be configured to use the same endpoint. You can use a ConsumerDefinition for each consumer and specify the same endpoint name in the constructor, or manually configure the endpoint. Both of these options are documented.

Related

Can API Key and JWT Token be used in the same .Net 6 WebAPI

I am building a new .Net 6 WebAPI that will be consumed by many applications so I need to implement API Keys to limit access to only those applications. Only a very small amount of the individual users will require authorization (admins) so I would like to combine with JWT for the Admin endpoints. We do not want to require users to have to crate an account where not necessary (non-admins). Is this possible? Thank You.
Yes it is possible.
The solution I recommend is to setup multiple authentication methods in asp.net core 6 using two authentication schemes that you have to specify inside Authorize attribute.
Here a simple implementation of ApiKey authentication:
namespace MyAuthentication;
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
private enum AuthenticationFailureReason
{
NONE = 0,
API_KEY_HEADER_NOT_PROVIDED,
API_KEY_HEADER_VALUE_NULL,
API_KEY_INVALID
}
private readonly Microsoft.Extensions.Logging.ILogger _logger;
private AuthenticationFailureReason _failureReason = AuthenticationFailureReason.NONE;
public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationOptions> options,
ILoggerFactory loggerFactory,
ILogger<ApiKeyAuthenticationHandler> logger,
UrlEncoder encoder,
ISystemClock clock) : base(options, loggerFactory, encoder, clock)
{
_logger = logger;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
//ApiKey header get
if (!TryGetApiKeyHeader(out string providedApiKey, out AuthenticateResult authenticateResult))
{
return authenticateResult;
}
//TODO: you apikey validity check
if (await ApiKeyCheckAsync(providedApiKey))
{
var principal = new ClaimsPrincipal(); //TODO: Create your Identity retreiving claims
var ticket = new AuthenticationTicket(principal, ApiKeyAuthenticationOptions.Scheme);
return AuthenticateResult.Success(ticket);
}
_failureReason = AuthenticationFailureReason.API_KEY_INVALID;
return AuthenticateResult.NoResult();
}
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
//Create response
Response.Headers.Append(HeaderNames.WWWAuthenticate, $#"Authorization realm=""{ApiKeyAuthenticationOptions.DefaultScheme}""");
Response.StatusCode = StatusCodes.Status401Unauthorized;
Response.ContentType = MediaTypeNames.Application.Json;
//TODO: setup a response to provide additional information if you want
var result = new
{
StatusCode = Response.StatusCode,
Message = _failureReason switch
{
AuthenticationFailureReason.API_KEY_HEADER_NOT_PROVIDED => "ApiKey not provided",
AuthenticationFailureReason.API_KEY_HEADER_VALUE_NULL => "ApiKey value is null",
AuthenticationFailureReason.NONE or AuthenticationFailureReason.API_KEY_INVALID or _ => "ApiKey is not valid"
}
};
using var responseStream = new MemoryStream();
await JsonSerializer.SerializeAsync(responseStream, result);
await Response.BodyWriter.WriteAsync(responseStream.ToArray());
}
protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
{
//Create response
Response.Headers.Append(HeaderNames.WWWAuthenticate, $#"Authorization realm=""{ApiKeyAuthenticationOptions.DefaultScheme}""");
Response.StatusCode = StatusCodes.Status403Forbidden;
Response.ContentType = MediaTypeNames.Application.Json;
var result = new
{
StatusCode = Response.StatusCode,
Message = "Forbidden"
};
using var responseStream = new MemoryStream();
await JsonSerializer.SerializeAsync(responseStream, result);
await Response.BodyWriter.WriteAsync(responseStream.ToArray());
}
#region Privates
private bool TryGetApiKeyHeader(out string apiKeyHeaderValue, out AuthenticateResult result)
{
apiKeyHeaderValue = null;
if (!Request.Headers.TryGetValue("X-Api-Key", out var apiKeyHeaderValues))
{
_logger.LogError("ApiKey header not provided");
_failureReason = AuthenticationFailureReason.API_KEY_HEADER_NOT_PROVIDED;
result = AuthenticateResult.Fail("ApiKey header not provided");
return false;
}
apiKeyHeaderValue = apiKeyHeaderValues.FirstOrDefault();
if (apiKeyHeaderValues.Count == 0 || string.IsNullOrWhiteSpace(apiKeyHeaderValue))
{
_logger.LogError("ApiKey header value null");
_failureReason = AuthenticationFailureReason.API_KEY_HEADER_VALUE_NULL;
result = AuthenticateResult.Fail("ApiKey header value null");
return false;
}
result = null;
return true;
}
private Task<bool> ApiKeyCheckAsync(string apiKey)
{
//TODO: setup your validation code...
return Task.FromResult<bool>(true);
}
#endregion
}
public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
public const string DefaultScheme = "ApiKey";
public static string Scheme => DefaultScheme;
public static string AuthenticationType => DefaultScheme;
}
public static class AuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddApiKeySupport(this AuthenticationBuilder authenticationBuilder, Action<ApiKeyAuthenticationOptions> options)
=> authenticationBuilder.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationOptions.DefaultScheme, options);
}
Then register inside builder setup:
_ = services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = ApiKeyAuthenticationOptions.DefaultScheme;
options.DefaultChallengeScheme = ApiKeyAuthenticationOptions.DefaultScheme;
})
.AddApiKeySupport(options => { });
You have to also setup the standard JWT Bearer validation (I don't post it for the sake of brevity).
To protect your endpoint add the Authorize attribute like:
[Authorize(AuthenticationSchemes = ApiKeyAuthenticationOptions.DefaultScheme)] //ApiKey
[HttpGet]
public async Task<IActionResult> Get()
{
//...omissis...
return null;
}
//or..
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] //Jwt
[HttpGet]
public async Task<IActionResult> Get()
{
//...omissis...
return null;
}
//or..
[Authorize(AuthenticationSchemes = $"{JwtBearerDefaults.AuthenticationScheme},{ApiKeyAuthenticationOptions.DefaultScheme}" )] //ApiKey and Jwt
[HttpGet]
public async Task<IActionResult> Get()
{
//...omissis...
return null;
}
For me it is the best way so as to carry out the authorization check before the start of the application pipeline (fail fast) and to be able to create the user identity.
But if you don't need to put informations about the Api Key inside the ClaimsPrincipal and only check the validity of Api Key the simplest way to do that is:
Protect the "admin" actions with JWT auth (with Authorize attribute)
Setup and register a middleware to only check the Api Key in all actions
Here is an example:
public class SimpleApiKeyMiddleware
{
private static readonly string API_KEY_HEADER = "X-Api-Key";
private readonly RequestDelegate _next;
private readonly ILogger<SimpleApiKeyMiddleware> _logger;
public SimpleApiKeyMiddleware(RequestDelegate next, ILogger<SimpleApiKeyMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task Invoke(HttpContext httpContext)
{
//Get apikey header
if (!httpContext.Request.Headers.TryGetValue(API_KEY_HEADER, out var apiKey))
{
_logger.LogError("ApiKey not found inside request headers");
//Error and exit from asp.net core pipeline
await GenerateForbiddenResponse(httpContext, "ApiKey not found inside request headers");
}
else if (!await ApiKeyCheckAsync(apiKey))
{
_logger.LogError("ApiKey is not valid: {ApiKey}", apiKey);
//Error and exit from asp.net core pipeline
await GenerateForbiddenResponse(httpContext, "ApiKey not valid");
}
else
{
_logger.LogInformation("ApiKey validated: {ApiKey}", apiKey);
//Proceed with pipeline
await _next(httpContext);
}
}
private Task<bool> ApiKeyCheckAsync(string apiKey)
{
//TODO: setup your validation code...
return Task.FromResult<bool>(true);
}
private async Task GenerateForbiddenResponse(HttpContext context, string message)
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
context.Response.ContentType = MediaTypeNames.Application.Json;
using var responseStream = new MemoryStream();
await System.Text.Json.JsonSerializer.SerializeAsync(responseStream, new
{
Status = StatusCodes.Status403Forbidden,
Message = message
});
await context.Response.BodyWriter.WriteAsync(responseStream.ToArray());
}
}
Registration:
_ = app.UseMiddleware<ApiKeyMiddleware>(); //Register as first middleware to avoid other middleware execution before api key check
Usage:
//Admin: Jwt and Api Key check
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] //Jwt and Api Key
[HttpGet]
public async Task<IActionResult> MyAdminApi()
{
//...omissis...
}
//Non Admin: Api Key check only
[HttpGet]
public async Task<IActionResult> MyNonAdminApi()
{
//...omissis...
}
Note: the middleware code above forces exit from pipeline returning an http result so as to stop next middleware execution. Also note that the asp.net core 6 pipeline executes Authorization first and then all the registered middlewares.

MassTransit Bus.Publish calls SendObserver

I have noticed that when i call Bus.Publish my SendObserver is beeing called along with my PublishObserver. In my original scenario i use the observers for some debug logging where i noticed that when i call Publish both the PublishObserver and the SendObserver is called with the same message. The example code below reproduces the scenario:
public class YourMessage { public string Text { get; set; } }
public class SendObserver : ISendObserver {
public Task PreSend<T>(SendContext<T> context) where T : class
{
return Task.CompletedTask;
}
public Task PostSend<T>(SendContext<T> context) where T : class
{
Console.Out.WriteLineAsync($"Message Sent, Id: {context.MessageId}");
return Task.CompletedTask;
}
public Task SendFault<T>(SendContext<T> context, Exception exception) where T : class
{
return Task.CompletedTask;
}
}
public class PublishObserver : IPublishObserver
{
public Task PrePublish<T>(PublishContext<T> context) where T : class
{
return Task.CompletedTask;
}
public Task PostPublish<T>(PublishContext<T> context) where T : class
{
Console.Out.WriteLineAsync($"Message Published, Id: {context.MessageId}");
return Task.CompletedTask;
}
public Task PublishFault<T>(PublishContext<T> context, Exception exception) where T : class
{
return Task.CompletedTask;
}
}
public class Program
{
public static void Main()
{
var bus = Bus.Factory.CreateUsingRabbitMq(sbc =>
{
var host = sbc.Host(new Uri("rabbitmq://rabbitmq/PublishSendTest"), h =>
{
h.Username("guest");
h.Password("guest");
});
sbc.ReceiveEndpoint(host, "test_queue", ep =>
{
ep.Handler<YourMessage>(context =>
{
return Console.Out.WriteLineAsync($"Received: {context.Message.Text}");
});
});
});
bus.ConnectSendObserver(new SendObserver());
bus.ConnectPublishObserver(new PublishObserver());
bus.Start();
bus.Publish(new YourMessage { Text = "Hi" });
Console.WriteLine("Press any key to exit");
Console.ReadKey();
bus.Stop();
}
}
Output:
Press any key to exit
Message Sent, Id: ac4f0000-3051-1065-bbe5-08d6335c9e05
Message Published, Id: ac4f0000-3051-1065-bbe5-08d6335c9e05
Received: Hi
Is this the expected behaviour? If so what can i do to determine if it acutally was a Publish call that created the message?
I used version 5.1.5
The inconsistent observer issue should be resolved in the develop builds, and a test has been created to verify the behavior on the supported transports. Once released, the send observer should only be called on an actual Send, and the publish observer should only be called on an actual Publish.
Thanks for bringing this up, I'm not sure how it got out of whack.

How to pass the user context details from from bot Controller to FormDialog

Bot Info
SDK Platform: .NET
Active Channels: Direct Line
Deployment Environment: Azure Bot Service
Question
How to pass user context details from from bot Controller to FormDialog?
Code Example
public virtual async Task < HttpResponseMessage > Post([FromBody] Activity activity) {
if (activity != null && activity.GetActivityType() == ActivityTypes.Message) {
await Conversation.SendAsync(activity, () => {
return Chain.From(() => FormDialog.FromForm(RequestOrder.BuildEnquiryForm));
});
} else {
HandleSystemMessage(activity);
}
return new HttpResponseMessage(System.Net.HttpStatusCode.OK);
}
public static IForm < RequestOrder > BuildEnquiryForm() {
return new FormBuilder < RequestOrder > ()
.Message("Hello {***Pass current user name?????****}Welcome to request bot!")
.Field(nameof(IsTermsAgreed))
.Field(nameof(ServiceRequired))
.AddRemainingFields()
.OnCompletion(ProcessParkingPermitRequest)
.Message("Thank you, I have submitted your request.")
.Build();
}
Fei Han answer is correct but using a static variable might lead to some unexpected error since all instances are sharing the same value. A better approach would be using the state of the form.
Request Order From
In your RequestOrder class you need to add a new member variable username.
public class RequestOrder
{
public string username;
/* Rest of your member variables */
}
The .Message method allows you to access the state of the form. You can get the username from the state of the form as below:
public static IForm < RequestOrder > BuildForm()
{
return new FormBuilder < RequestOrder > ()
.Message(async (state) => {
return new PromptAttribute($"Hi {state.username}, Welcome to request bot! ");
})
.Field(nameof(IsTermsAgreed))
.Field(nameof(ServiceRequired))
.AddRemainingFields()
.OnCompletion(ProcessParkingPermitRequest)
.Message("Thank you, I have submitted your request.")
.Build();
}
Root Dialog
In your root Dialog, before calling the BuildForm you need to create a new instance of your RequestOrder class and initialize username as the current user's name. Then pass your form to the BuildForm with option FormOptions.PromptInStart.
[Serializable]
public class RootDialog : IDialog<object>
{
public Task StartAsync(IDialogContext context)
{
context.Wait(MessageReceivedAsync);
return Task.CompletedTask;
}
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
var form = new RequestOrder()
{
username = context.Activity.From.Id
};
var requestOrderform = new FormDialog<RequestOrder>(form, RequestOrder.BuildForm, FormOptions.PromptInStart);
context.Call<RequestOrder>(requestOrderform, SampleFormSubmitted);
}
private async Task SampleFormSubmitted(IDialogContext context, IAwaitable<SampleForm> result)
{
try
{
var query = await result;
context.Done(true);
}
catch (FormCanceledException<SampleForm> e)
{
string reply;
if (e.InnerException == null)
{
reply = $"You quit. Maybe you can fill some other time.";
}
else
{
reply = $"Something went wrong. Please try again.";
}
context.Done(true);
await context.PostAsync(reply);
}
}
}
This is what you get:
In following sample code, I define a constructor of SandwichOrder class with a string type parameter, then I call FormDialog as a child dialog from root dialog (not from Messages Controller directly) and pass user name as parameter, which works for me, you can refer to it.
In RootDialog:
[Serializable]
public class RootDialog : IDialog<object>
{
public Task StartAsync(IDialogContext context)
{
context.Wait(MessageReceivedAsync);
return Task.CompletedTask;
}
private async Task MessageReceivedAsync(IDialogContext context, IAwaitable<object> result)
{
var username = "Fei Han";
var myform = new Microsoft.Bot.Builder.FormFlow.FormDialog<SandwichOrder>(new SandwichOrder($"{username}"), SandwichOrder.BuildForm, Microsoft.Bot.Builder.FormFlow.FormOptions.PromptInStart, null);
context.Call<SandwichOrder>(myform, FormCompleteCallback);
}
private async Task FormCompleteCallback(IDialogContext context, IAwaitable<SandwichOrder> result)
{
await context.PostAsync($"The form is completed!");
context.Done(this);
}
}
In SandwichOrder class:
namespace BotFormFlowTest
{
public enum SandwichOptions
{
BLT, BlackForestHam, BuffaloChicken, ChickenAndBaconRanchMelt, ColdCutCombo, MeatballMarinara,
OvenRoastedChicken, RoastBeef, RotisserieStyleChicken, SpicyItalian, SteakAndCheese, SweetOnionTeriyaki, Tuna,
TurkeyBreast, Veggie
};
public enum LengthOptions { SixInch, FootLong };
public enum BreadOptions { NineGrainWheat, NineGrainHoneyOat, Italian, ItalianHerbsAndCheese, Flatbread };
public enum CheeseOptions { American, MontereyCheddar, Pepperjack };
public enum ToppingOptions
{
Avocado, BananaPeppers, Cucumbers, GreenBellPeppers, Jalapenos,
Lettuce, Olives, Pickles, RedOnion, Spinach, Tomatoes
};
public enum SauceOptions
{
ChipotleSouthwest, HoneyMustard, LightMayonnaise, RegularMayonnaise,
Mustard, Oil, Pepper, Ranch, SweetOnion, Vinegar
};
[Serializable]
public class SandwichOrder
{
public static string username = "User";
public SandwichOrder(string uname)
{
username = uname;
}
public SandwichOptions? Sandwich;
public LengthOptions? Length;
public BreadOptions? Bread;
public CheeseOptions? Cheese;
public List<ToppingOptions> Toppings;
public List<SauceOptions> Sauce;
public static IForm<SandwichOrder> BuildForm()
{
return new FormBuilder<SandwichOrder>()
.Message($"Hello {username}, Welcome to the simple sandwich order bot!")
.Build();
}
};
}
Test Result:

RabbitMq Consumers do Not Consume

public class RequestConsumer :
IConsumer<StartFlowCommand>,
IConsumer<List<StartAndNextCommand>>
{
readonly IWorkFlowHandler _flowHandler;
public RequestConsumer(IContainer container)
{
_flowHandler = container.Resolve<IWorkFlowHandler>();
}
public async Task Consume(ConsumeContext<StartAndNextCommand> context)
{
var result =await _flowHandler.WorkFlowStartNext(context.Message);
await context.RespondAsync(result);
}
public async Task Consume(ConsumeContext<List<StartAndNextCommand>> context)
{
var result = await Task.Run(() => _flowHandler.WorkFlowStartNextBatch(context.Message));
await context.RespondAsync(result);
}
Message type of StartAndNextCommand can consume,but type of List are unable to consume,why?
This is by design. We can only consume one message. You can have a new contract, like:
public interface StartAndNextBatch
{
IList<StartAndNextCommand> Commands { get; }
}
and then have a consumer for that message type
public async Task Consume(ConsumeContext<StartAndNextBatch> context)
but you also need to publish that message type
await bus.Publish<StartAndNextBatch>(
new { Commands = ... }
);

MassTransit's ISendObserver is not observing

I have a consumer that is also publishing a response back to the bus. I can get an IReceiveObserver wired up and working on the bus, but I haven't been able to get either an ISendObserver or IPublishObserver running. I have confirmed with RabbitMQ management console that the messages are being published correctly.
class Program
{
static BusHandle _BusHandle;
static void Main(string[] args)
{
InitLogging();
InitStructureMap();
InitBus();
System.Console.WriteLine("Starting processing, ENTER to stop...");
System.Console.ReadLine();
System.Console.WriteLine("See you later, alligator!");
StopBus();
}
static void InitBus()
{
var busCtrl = ObjectFactory.Container.GetInstance<IBusControl>();
var recObserver = ObjectFactory.Container.GetInstance<IReceiveObserver>();
var sendObserver = ObjectFactory.Container.GetInstance<ISendObserver>();
busCtrl.ConnectReceiveObserver(recObserver);
busCtrl.ConnectSendObserver(sendObserver);
_BusHandle = busCtrl.Start();
}
static void StopBus()
{
_BusHandle.Stop();
}
static void InitLogging()
{
XmlConfigurator.Configure();
Log4NetLogger.Use();
}
static void InitStructureMap()
{
ObjectFactory.Initialize(x => {
x.AddRegistry<MyTestConsoleRegistry>();
x.AddRegistry<MyTestRegistry>();
});
}
}
public class MyTestConsoleRegistry : Registry
{
public MyTestConsoleRegistry()
{
var rabbitURI = ConfigurationManager.AppSettings["rabbitMQHostUri"];
var queueName = ConfigurationManager.AppSettings["massTransitQueue"];
For<IBusControl>(new SingletonLifecycle())
.Use("Configure IBusControl for MassTransit consumers with RabbitMQ transport",
ctx => Bus.Factory.CreateUsingRabbitMq(cfg => {
cfg.UseJsonSerializer();
cfg.PublisherConfirmation = true;
var host = cfg.Host(new Uri(rabbitURI), rabbitCfg => { });
cfg.ReceiveEndpoint(host, queueName, endpointCfg => {
endpointCfg.LoadFrom(ctx);
});
})
);
For<IReceiveObserver>().Use<MassTransitObserver>();
For<ISendObserver>().Use<MassTransitObserver>();
// ...snip...
}
}
public class MyTestRegistry : Registry
{
public MyTestRegistry()
{
ForConcreteType<MyTestConsumer>();
// ...snip...
}
}
public class MassTransitObserver : IReceiveObserver, ISendObserver
{
// Does nothing for now, just trying to wire it up...
public Task ConsumeFault<T>(ConsumeContext<T> context, TimeSpan duration, string consumerType, Exception exception) where T : class
{
return Task.CompletedTask;
}
public Task PostConsume<T>(ConsumeContext<T> context, TimeSpan duration, string consumerType) where T : class
{
return Task.CompletedTask;
}
public Task PostReceive(ReceiveContext context)
{
return Task.CompletedTask;
}
public Task PreReceive(ReceiveContext context)
{
return Task.CompletedTask;
}
public Task ReceiveFault(ReceiveContext context, Exception exception)
{
return Task.CompletedTask;
}
public Task PreSend<T>(SendContext<T> context) where T : class
{
return Task.CompletedTask;
}
public Task PostSend<T>(SendContext<T> context) where T : class
{
return Task.CompletedTask;
}
public Task SendFault<T>(SendContext<T> context, Exception exception) where T : class
{
return Task.CompletedTask;
}
}
public class MyTestConsumer : IConsumer<MyTestMessage>,
// for testing only:
IConsumer<MyTestResponse>
{
readonly IDoSomething _DoSomething;
public TestConsumer(IDoSomething doSomething)
{
_DoSomething = doSomething;
}
public Task Consume(ConsumeContext<MyTestResponse> context)
{
// For testing only...
return Task.CompletedTask;
}
public async Task Consume(ConsumeContext<MyTestMessage> context)
{
var result = await _DoSomething(context.Message.Id);
var resp = new MyTestResponseMessage(result);
await context
.Publish<MyTestResponse>(resp);
}
}
Given this code, the IReceiveObserver methods are getting called, but the ISendObserver methods are not.
I'm new to MassTransit, I expect this is probably a straightforward issue.
EDIT: A unit test using NUnit and Moq, doesn't use StructureMap. I believe this properly illustrates what I'm seeing.
[Test]
public void TestSendObserver()
{
var bus = CreateBus();
var busHandle = bus.Start();
var sendObs = new Mock<ISendObserver>();
sendObs.Setup(x => x.PreSend<TestMessage>(It.IsAny<SendContext<TestMessage>>()))
.Returns(Task.FromResult(0))
.Verifiable();
sendObs.Setup(x => x.PostSend<TestMessage>(It.IsAny<SendContext<TestMessage>>()))
.Returns(Task.FromResult(0))
.Verifiable();
using (bus.ConnectSendObserver(sendObs.Object)) {
var pubTask = bus.Publish(new TestMessage { Message = "Some test message" });
pubTask.Wait();
}
busHandle.Stop();
// Fails, neither PreSend nor PostSend have been called
sendObs.Verify(x => x.PreSend<TestMessage>(It.IsAny<SendContext<TestMessage>>()), Times.Once());
sendObs.Verify(x => x.PostSend<TestMessage>(It.IsAny<SendContext<TestMessage>>()), Times.Once());
}
IBusControl CreateBus()
{
return MassTransit.Bus.Factory.CreateUsingRabbitMq(x => {
var host = x.Host(new Uri("rabbitmq://localhost/"), h => {
h.Username("guest");
h.Password("guest");
});
});
}
public class TestMessage
{
public String Message { get; set; }
}

Resources