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.
Related
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.
I am trying to incorporate DomainService into my application, and tried to do it like the code below shows.
Here is the sample code for the manager:
namespace FlexSped.DefaultColors
{
public class DefaultColorManager : FlexSpedDomainServiceBase, IDefaultColorsManager
{
private readonly IRepository<DefaultColor> _defaultColorRepository;
public DefaultColorManager(IRepository<DefaultColor> defColorRep)
{
_defaultColorRepository = defColorRep;
}
public async Task Create(DefaultColor input)
{
await _defaultColorRepository.InsertAsync(input);
}
public Task Update(int id)
{
throw new NotImplementedException();
}
}
}
And this is the application service:
namespace FlexSped.DefaultColors
{
[AbpAuthorize(AppPermissions.Pages_Administration_DefaultColors)]
public class DefaultColorsAppService : FlexSpedAppServiceBase, IDefaultColorsAppService
{
private readonly IDefaultColorsManager _defaultColorManager;
private readonly IRepository<DefaultColor> _defaultColorRepository;
//private readonly IIocResolver _iocResolver;
public DefaultColorsAppService(IRepository<DefaultColor> defaultColorRepository, IDefaultColorsManager defColManager)
{
_defaultColorRepository = defaultColorRepository;
_defaultColorManager = defColManager;
//_iocResolver = iocResolver;
}
public async Task CreateOrEdit(CreateOrEditDefaultColorDto input)
{
if (input.Id == null)
{
await Create(input);
}
else
{
await Update(input);
}
}
[AbpAuthorize(AppPermissions.Pages_Administration_DefaultColors_Create)]
private async Task Create(CreateOrEditDefaultColorDto input)
{
DefaultColor dt = ObjectMapper.Map<DefaultColor>(input);
await _defaultColorManager.Create(dt);
}
}
}
All this produces this error:
'FlexSped.DefaultColors.DefaultColorsAppService' is waiting for the following dependencies:
- Service 'FlexSped.DefaultColors.IDefaultColorsManager' which was not registered.
Not sure what the problem is.I was following convention here.
ABPboiler is registering the services dependencies by name so your should match the implementation with definition. In your case:
IDefaultColors.IDefaultColorsManager should be IDefaultColors.IDefaultColorManager
or vice versa DefaultColorManager should be DefaultColorsManager.
I have an application that needs to intercept the current message consume context and extract a value that is defined in a base interface. That value is a tenant code that is eventually used in an EF database context.
I have a provider that takes a MassTransit ConsumerContext, and then using context.TryGetMessage(), extracts the tenant code, which is ultimately used to switch database contexts to a specific tenant database.
The issue lies in the MessageContextTenantProvider below. If a non-fault message is consumed then ConsumeContext<IBaseEvent> works fine. However if it is a fault, ConsumeContext<Fault<IBaseEvent>> doesn't work as expected.
Durring debugging I can see that the message context for a fault is ConsumeContext<Fault<IVerifyEvent>>, but why doesn't it work with a base interface as per the standard message? Of course, ConsumeContext<Fault<IVerifiedEvent>> works fine, but I have a lot of message types, and I don't want to have to define them all in that tenant provider.
Any ideas?
public interface ITenantProvider
{
string GetTenantCode();
}
public class MessageContextTenantProvider : ITenantProvider
{
private readonly ConsumeContext _consumeContext;
public MessageContextTenantProvider(ConsumeContext consumeContext)
{
_consumeContext = consumeContext;
}
public string GetTenantCode()
{
// get tenant from message context
if (_consumeContext.TryGetMessage(out ConsumeContext<IBaseEvent> baseEvent))
{
return baseEvent.Message.TenantCode; // <-- works for the non fault consumers
}
// get tenant from fault message context
if (_consumeContext.TryGetMessage<Fault<IBaseEvent>>(out var gebericFaultEvent))
{
return gebericFaultEvent.Message.Message.TenantCode; // <- doesn't work generically
}
// get tenant from fault message context (same as above)
if (_consumeContext.TryGetMessage(out ConsumeContext<Fault<IBaseEvent>> faultEvent))
{
return faultEvent.Message.Message.TenantCode; // <= generically doesn't work when using the base interface?
}
// get tenant from specific concrete fault class
if (_consumeContext.TryGetMessage(out ConsumeContext<Fault<IVerifiedEvent>> verifiedFaultEvent))
{
return verifiedFaultEvent.Message.Message.TenantCode; // <-- this works
}
// not able to extract tenant
return null;
}
}
public partial class VerificationDbContext
{
string connectionString;
public string ConnectionString
{
get
{
if (connectionString == null)
{
string tenantCode = _tenantProvider.GetTenantCode();
connectionString = _tenantConnectionManager.GetConnectionString(orgId);
}
return connectionString;
}
}
private readonly ITenantProvider _tenantProvider;
private readonly ITenantConnectionManager _tenantConnectionManager;
public VerificationDbContext(ITenantProvider tenantProvider, ITenantConnectionManager tenantConnectionManager)
{
_tenantProvider = tenantProvider;
_tenantConnectionManager = tenantConnectionManager;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (string.IsNullOrEmpty(this.ConnectionString))
{
optionsBuilder.UseSqlServer(#"Data Source=.\SQLEXPRESS;Initial Catalog=VerificationDb;Integrated Security=True")
.ConfigureWarnings((warningBuilder) => warningBuilder.Ignore(RelationalEventId.AmbientTransactionWarning));
}
else
{
optionsBuilder.UseSqlServer(this.ConnectionString)
.ConfigureWarnings((warningBuilder) => warningBuilder.Ignore(RelationalEventId.AmbientTransactionWarning));
}
}
}
public interface ITenantConnectionManager
{
string GetConnectionString(string tenantCode);
}
public class TenantConnectionManager : ITenantConnectionManager
{
private ITenantRepository _tenantRepository;
public TenantConnectionManager(ITenantRepository tenantRepository)
{
_tenantRepository = tenantRepository;
}
public string GetConnectionString(string tenantCode)
{
return _tenantRepository.GetByTenantCode(tenantCode).ConnectionString;
}
}
public interface IBaseEvent
{
string TenantCode { get; }
}
public interface IVerifiedEvent : IBaseEvent
{
string JobReference { get; }
}
public class VerifiedEventConsumer : IConsumer<IVerifiedEvent>
{
private readonly IVerifyCommand _verifyCommand;
private readonly ITenantProvider _tenantProvider;
public VerifiedEventConsumer(ITenantProvider tenantProvider, IVerifyCommand verifyCommand)
{
_verifyCommand = verifyCommand;
_tenantProvider = tenantProvider;
}
public async Task Consume(ConsumeContext<IVerifiedEvent> context)
{
await _verifyCommand.Execute(new VerifyRequest
{
JobReference = context.Message.JobReference,
TenantCode = context.Message.TenantCode
});
}
}
public class VerifiedEventFaultConsumer : IConsumer<Fault<IVerifiedEvent>>
{
private readonly IVerifyFaultCommand _verifyFaultCommand;
private readonly ITenantProvider _tenantProvider;
public CaseVerifiedEventFaultConsumer(ITenantProvider tenantProvider, IVerifyFaultCommand verifyFaultCommand)
{
_verifyFaultCommand = verifyFaultCommand;
_tenantProvider = tenantProvider;
}
public async Task Consume(ConsumeContext<Fault<ICaseVerifiedEvent>> context)
{
await _verifyFaultCommand.Execute(new VerifiedFaultRequest
{
JobReference = context.Message.Message.JobReference,
Exceptions = context.Message.Exceptions
});
}
}
I've solved the issue by using the GreenPipes TryGetPayload extension method:
public class MessageContextTenantProvider : ITenantProvider
{
private readonly ConsumeContext _consumeContext;
public MessageContextTenantProvider(ConsumeContext consumeContext)
{
_consumeContext = consumeContext;
}
public string GetTenantCode()
{
// get tenant from message context
if (_consumeContext.TryGetMessage(out ConsumeContext<IBaseEvent> baseEvent))
{
return baseEvent.Message.TenantCode;
}
// get account code from fault message context using Greenpipes
if (_consumeContext.TryGetPayload(out ConsumeContext<Fault<IBaseEvent>> payloadFaultEvent))
{
return payloadFaultEvent.Message.Message.TenantCode;
}
// not able to extract tenant
return null;
}
}
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:
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; }
}