Masstransit Deserialization Issue With Encryption enabled in Amazon SQS - masstransit

Object deserialization is working without encryption in both RabbitMQ and Amazon SQS in Masstransit and also Object deserialization working fine in case of encryption enabled with RabbitMQ, But getting deserialization error in case of encryption enabled with Amazon SQS.
Getting below error while deserialization :
System.Runtime.Serialization.SerializationException: An exception occurred while deserializing the message envelope
---> System.ArgumentOutOfRangeException: Unexpected BsonType value: 38 (Parameter 'type')
at Newtonsoft.Json.Bson.BsonDataReader.ReadType(BsonType type)
at Newtonsoft.Json.Bson.BsonDataReader.ReadNormal()
at Newtonsoft.Json.Bson.BsonDataReader.Read()
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateObject(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.DeserializeConvertable(JsonConverter converter, JsonReader reader, Type objectType, Object existingValue)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
at MassTransit.Serialization.EncryptedMessageDeserializerV2.Deserialize(MessageBody body, Headers headers, Uri destinationAddress) in /_/src/MassTransit.Newtonsoft/Serialization/EncryptedMessageDeserializerV2.cs:line 47
--- End of inner exception stack trace ---
at MassTransit.Serialization.EncryptedMessageDeserializerV2.Deserialize(MessageBody body, Headers headers, Uri destinationAddress) in /_/src/MassTransit.Newtonsoft/Serialization/EncryptedMessageDeserializerV2.cs:line 62
at MassTransit.Serialization.EncryptedMessageDeserializerV2.Deserialize(ReceiveContext receiveContext) in /_/src/MassTransit.Newtonsoft/Serialization/EncryptedMessageDeserializerV2.cs:line 36
at MassTransit.Middleware.DeserializeFilter.Send(ReceiveContext context, IPipe`1 next) in /_/src/MassTransit/Middleware/DeserializeFilter.cs:line 36
at MassTransit.Middleware.RescueFilter`2.MassTransit.IFilter<TContext>.Send(TContext context, IPipe`1 next) in /_/src/MassTransit/Middleware/RescueFilter.cs:line 43
Below Are Amazon SQS Configuration :
public class Program
{
public static async Task Main(string[] args)
{
await Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddMassTransit(x =>
{
x.AddConsumers(typeof(SampleCommandConsumer).Assembly);
x.UsingAmazonSqs(
(context, sqsConfig) =>
{
sqsConfig.Host(
Environment.GetEnvironmentVariable("AWS_DEFAULT_REGION"),
h =>
{
var accessKey = Environment.GetEnvironmentVariable("AWS_ACCESS_KEY_ID");
h.AccessKey(accessKey);
var env = Environment.GetEnvironmentVariable("AWS_ENV_PREFIX");
h.Scope(env, true);
var secret = Environment.GetEnvironmentVariable("AWS_SECRET_ACCESS_KEY");
var serviceUrl = Environment.GetEnvironmentVariable("AWS_SQS_URL");
if (!string.IsNullOrEmpty(secret))
{
h.SecretKey(secret);
}
if (!string.IsNullOrEmpty(serviceUrl))
{
h.Config(new AmazonSQSConfig { ServiceURL = serviceUrl });
}
});
byte[] key = { 156, 62, 75, 207, 47, 62, 42, 73, 53, 164, 114, 49, 46, 226, 164, 1, 79, 225, 65, 193, 59, 192, 72, 27, 234, 87, 17, 44, 46, 207, 137, 1 };
sqsConfig.UseJsonSerializer();
sqsConfig.UseJsonDeserializer();
sqsConfig.UseEncryption(key);
var prefix = $"{Environment.GetEnvironmentVariable("ENVIRONMENT")}_{Environment.GetEnvironmentVariable("SERVICE_ACCOUNT_CLIENT_ID")}";
sqsConfig.ConfigureEndpoints(
context,
new KebabCaseEndpointNameFormatter(prefix, false)
);
}
);
});
services.AddHostedService<Publisher>();
})
.Build()
.RunAsync();
}
}
Publish Contract:
public class SampleCommand
{
/// <summary>
/// The correlation id events published as a result of processing this command must include.
/// </summary>
public Guid CorrelationId { get; init; }
/// <summary>
/// The identifer of the Sender that is to be authorized to use the listed message features.
/// </summary>
public long SenderId { get; set; }
}
Consumer:
public class SampleCommandConsumer : IConsumer<SampleCommand>
{
readonly IBus bus;
/// <summary>
/// Initializes a new instance of the <see cref="MessageDeliveredConsumer" /> class.
/// </summary>
public SampleCommandConsumer(IBus bus)
{
this.bus = bus;
}
public async Task Consume(ConsumeContext<SampleCommand> context)
{
await context.Publish(new SampleEvent()
{
CorrelationId = context.CorrelationId.Value,
});
}
}
configured Amazon SQS as per masstransit documentation, but didn't workout

Amazon SQS doesn't allow binary messages, neither does SNS. You should enable encryption at the queue level in SQS if you want message encryption.
You can configure queue encryption using a configuration callback:
x.AddConfigureEndpointCallback((_,cfg) =>
{
if (cfg is IAmazonSqsReceiveEndpointConfigurator sqs)
sqs.QueueAttributes.Add(QueueAttributeName.KmsMasterKeyId, "arn of key");
});

Related

.NET5 Web Api how to allow Guid in any format

I have a Dto that looks like this:
public class CreateObjectDto
{
public Guid SomeGuid { get; set; }
}
problem I'm having is that default Guid converter does not allow values outside "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx" format, so users are required to enter Guid with dashes which is not desirable. I would like an option to parse any regular Guid whether it has dashes or not.
Error if I call api with different Guid format is:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-1714eba1650b1548afd8581204d38a0c-ffc921fac3022540-00",
"errors": {
"$.attachmentList[0].documentId": [
"The JSON value could not be converted to System.Guid. Path: $.attachmentList[0].documentId | LineNumber: 0 | BytePositionInLine: 292."
]
}
}
EDIT with solution
(modified accepted solution a bit):
CustomGuidConverter:
public class CustomGuidConverter : JsonConverter<Guid>
{
public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (!Guid.TryParse(reader.GetString(), out var parsedGuid))
{
throw new Exception($"Unable to parse {reader.GetString()} to GUID");
}
return parsedGuid;
}
public override void Write(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToString("D"));
}
startup.cs:
// add this line to apply conversion globally and not only for one property
services.AddMvc().AddJsonOptions(opts =>
{
opts.JsonSerializerOptions.Converters.Add(new CustomGuidConverter());
});
You have to use a custom JsonConverter https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-6-0 for the given type (Guid)
public class CustomGuidJsonConverter : JsonConverter<Guid>
{
private Regex _uwCharsRegex = new Regex("[ \t-]");
private Regex _validityRegex = new Regex("[a-f0-9]{32}");
public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
string value = _uwCharsRegex.Replace(reader.GetString(), "").ToLower();
// Check validity
if (!_validityRegex.IsMatch(value))
{
return Guid.Empty; // or throw exception
}
return new Guid(value);
}
public override void Write(Utf8JsonWriter writer, Guid guidValue, JsonSerializerOptions options)
=> writer.WriteStringValue(guidValue.ToString());
}
Then you can use it by annoting the wanted Guid property.
public class CreateObjectDto
{
[JsonConverter(typeof(CustomGuidJsonConverter))]
public Guid SomeGuid { get; set; }
}

Sorting in elastic Search by Location and getting error while location lat and long is not coming

I have tried with the below code full fill my location sorting requirements in elastic search, when location is coming then it's working fine, but when location lat and long is not coming then my Nest c# code giving me the Exception
.Sort(s = >s.Ascending(fs = >fs.current_stage).Field(gh = >{
var query = new SortFieldDescriptor < CarSearchRequest > ();
if (searchAjaxRequest.sortingFilter != null) {
if (searchAjaxRequest.sortingFilter == "titleasc") {
query = gh.Field(tt = >tt.model).Order(SortOrder.Ascending);
}
else if (searchAjaxRequest.sortingFilter == "titledsc") {
query = gh.Field(tt = >tt.model).Order(SortOrder.Descending);
}
else if (searchAjaxRequest.sortingFilter == "priceasc") {
query = gh.Field(tt = >tt.price).Order(SortOrder.Ascending);
}
else if (searchAjaxRequest.sortingFilter == "Pricedsc") {
query = gh.Field(tt = >tt.price).Order(SortOrder.Descending);
}
else if (searchAjaxRequest.sortingFilter == "Kmasc") {
query = gh.Field(tt = >tt.kmRun).Order(SortOrder.Ascending);
}
else if (searchAjaxRequest.sortingFilter == "kmdsc") {
query = gh.Field(tt = >tt.kmRun).Order(SortOrder.Descending);
}
else if (searchAjaxRequest.sortingFilter == "introductiondateasc") {
query = gh.Field(tt = >tt.mfYear).Order(SortOrder.Ascending);
}
else if (searchAjaxRequest.sortingFilter == "introductiondatedesc") {
query = gh.Field(tt = >tt.mfYear).Order(SortOrder.Descending);
}
return query;
}
else if (searchAjaxRequest.category != null) {
if (searchAjaxRequest.category == "recentViewed") {
query = gh.Field(tt = >tt.createdDate).Order(SortOrder.Descending);
}
else if (searchAjaxRequest.category == "topRated") {
query = gh.Field(tt = >tt.overallRating).Order(SortOrder.Descending);
}
return query;
}
return query;
}).GeoDistance(g = >{
var locatioSorting = new SortGeoDistanceDescriptor < CarSearchRequest > ();
if (searchAjaxRequest.location != null) {
locatioSorting = g.Field(ab = >ab.location).Order(SortOrder.Ascending).Unit(DistanceUnit.Kilometers).Mode(SortMode.Min).Points(new GeoLocation(searchAjaxRequest.location.lat, searchAjaxRequest.location.lon));
}
return locatioSorting;
})));
Exception which is coming after null handle in my code.
System.NullReferenceException: Object reference not set to an instance of an object.
at Nest.FieldResolver.Resolve(Field field)
at Nest.SortJsonConverter.<>c__DisplayClass6_1.<WriteJson>b__0(JsonWriter w)
at Nest.ReserializeJsonConverter`2.Reserialize(JsonWriter writer, Object value, JsonSerializer serializer, Action`1 inlineWriter)
at Nest.SortJsonConverter.WriteJson(JsonWriter writer, Object value, JsonSerializer serializer)
at Elastic.Internal.JsonNet.Serialization.JsonSerializerInternalWriter.SerializeConvertable(JsonWriter writer, JsonConverter converter, Object value, JsonContract contract, JsonContainerContract collectionContract, JsonProperty containerProperty)
at Elastic.Internal.JsonNet.Serialization.JsonSerializerInternalWriter.SerializeList(JsonWriter writer, IEnumerable values, JsonArrayContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)
at Elastic.Internal.JsonNet.Serialization.JsonSerializerInternalWriter.SerializeObject(JsonWriter writer, Object value, JsonObjectContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)
at Elastic.Internal.JsonNet.Serialization.JsonSerializerInternalWriter.Serialize(JsonWriter jsonWriter, Object value, Type objectType)
at Elastic.Internal.JsonNet.JsonSerializer.SerializeInternal(JsonWriter jsonWriter, Object value, Type objectType)
at Nest.InternalSerializer.Serialize[T](T data, Stream writableStream, SerializationFormatting formatting)
at Elasticsearch.Net.SerializableData`1.Write(Stream writableStream, IConnectionConfigurationValues settings)
at Elasticsearch.Net.HttpWebRequestConnection.Request[TResponse](RequestData requestData)
at Elasticsearch.Net.RequestPipeline.CallElasticsearch[TResponse](RequestData requestData)
at Elasticsearch.Net.Transport`1.Request[TResponse](HttpMethod method, String path, PostData data, IRequestParameters requestParameters)
# Inner Exception: Object reference not set to an instance of an object.
System.NullReferenceException: Object reference not set to an instance of an object.
at Nest.FieldResolver.Resolve(Field field)
at Nest.SortJsonConverter.<>c__DisplayClass6_1.<WriteJson>b__0(JsonWriter w)
at Nest.ReserializeJsonConverter`2.Reserialize(JsonWriter writer, Object value, JsonSerializer serializer, Action`1 inlineWriter)
at Nest.SortJsonConverter.WriteJson(JsonWriter writer, Object value, JsonSerializer serializer)
at Elastic.Internal.JsonNet.Serialization.JsonSerializerInternalWriter.SerializeConvertable(JsonWriter writer, JsonConverter converter, Object value, JsonContract contract, JsonContainerContract collectionContract, JsonProperty containerProperty)
at Elastic.Internal.JsonNet.Serialization.JsonSerializerInternalWriter.SerializeList(JsonWriter writer, IEnumerable values, JsonArrayContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)
at Elastic.Internal.JsonNet.Serialization.JsonSerializerInternalWriter.SerializeObject(JsonWriter writer, Object value, JsonObjectContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)
at Elastic.Internal.JsonNet.Serialization.JsonSerializerInternalWriter.Serialize(JsonWriter jsonWriter, Object value, Type objectType)
at Elastic.Internal.JsonNet.JsonSerializer.SerializeInternal(JsonWriter jsonWriter, Object value, Type objectType)
at Nest.InternalSerializer.Serialize[T](T data, Stream writableStream, SerializationFormatting formatting)
at Elasticsearch.Net.SerializableData`1.Write(Stream writableStream, IConnectionConfigurationValues settings)
at Elasticsearch.Net.HttpWebRequestConnection.Request[TResponse](RequestData requestData)
at Elasticsearch.Net.RequestPipeline.CallElasticsearch[TResponse](RequestData requestData)
at Elasticsearch.Net.Transport`1.Request[TResponse](HttpMethod method, String path, PostData data, IRequestParameters requestParameters)
# Exception:
Elasticsearch.Net.UnexpectedElasticsearchClientException: Object reference not set to an instance of an object. ---> System.NullReferenceException: Object reference not set to an instance of an object.
at Nest.FieldResolver.Resolve(Field field)
at Nest.SortJsonConverter.<>c__DisplayClass6_1.<WriteJson>b__0(JsonWriter w)
at Nest.ReserializeJsonConverter`2.Reserialize(JsonWriter writer, Object value, JsonSerializer serializer, Action`1 inlineWriter)
at Nest.SortJsonConverter.WriteJson(JsonWriter writer, Object value, JsonSerializer serializer)
at Elastic.Internal.JsonNet.Serialization.JsonSerializerInternalWriter.SerializeConvertable(JsonWriter writer, JsonConverter converter, Object value, JsonContract contract, JsonContainerContract collectionContract, JsonProperty containerProperty)
at Elastic.Internal.JsonNet.Serialization.JsonSerializerInternalWriter.SerializeList(JsonWriter writer, IEnumerable values, JsonArrayContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)
at Elastic.Internal.JsonNet.Serialization.JsonSerializerInternalWriter.SerializeObject(JsonWriter writer, Object value, JsonObjectContract contract, JsonProperty member, JsonContainerContract collectionContract, JsonProperty containerProperty)
at Elastic.Internal.JsonNet.Serialization.JsonSerializerInternalWriter.Serialize(JsonWriter jsonWriter, Object value, Type objectType)
at Elastic.Internal.JsonNet.JsonSerializer.SerializeInternal(JsonWriter jsonWriter, Object value, Type objectType)
at Nest.InternalSerializer.Serialize[T](T data, Stream writableStream, SerializationFormatting formatting)
at Elasticsearch.Net.SerializableData`1.Write(Stream writableStream, IConnectionConfigurationValues settings)
at Elasticsearch.Net.HttpWebRequestConnection.Request[TResponse](RequestData requestData)
at Elasticsearch.Net.RequestPipeline.CallElasticsearch[TResponse](RequestData requestData)
at Elasticsearch.Net.Transport`1.Request[TResponse](HttpMethod method, String path, PostData data, IRequestParameters requestParameters)
--- End of inner exception stack trace ---

RegionInfo serializing in ElasticSearch

It seems that the RegionInfo object has kinda been forgotten in terms of serialization. CultureInfo works great and is serialized to and from a string. When attempting to throw in a RegionInfo object, I get a mess of all the properties of RegionInfo that can't be deserialized because there is no constructor that takes all those properties in reverse. I would love to just serialize and deserialize the RegionInfos as strings, like CultureInfo, but can't quite figure that out.
My attempt:
I created a regioninfo converter
public class RegionInfoConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, ((RegionInfo)value).Name);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var token = JToken.Load(reader);
return new RegionInfo(token.ToObject<string>());
}
public override bool CanConvert(Type objectType)
{
return typeof(RegionInfo) == objectType;
}
}
I stuffed that into the ConnectionSettings:
var connectionSettings = new ConnectionSettings(pool,
(builtin, settings) => new JsonNetSerializer(
builtin,
settings,
contractJsonConverters: new JsonConverter[] { new RegionInfoConverter() })
);
but I get the error: object mapping for [region] tried to parse field [region] as object, but found a concrete value
That sounds like one of my serializer pieces is wrong, but I don't feel like I quite understand enough to figure out which part it is. Thanks.
I think the issue here may be that Elasticsearch has initially inferred an object datatype mapping for RegionInfo from a document to be indexed, and is now being passed a string value for RegionInfo. You may need to delete the index and create again, mapping the RegionInfo property as a keyword datatype.
Here's a working example
private static void Main()
{
var defaultIndex = "my_index";
var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
var settings = new ConnectionSettings(pool, (b, s) =>
new JsonNetSerializer(b, s, contractJsonConverters: new JsonConverter[] { new RegionInfoConverter() })
)
.DefaultIndex(defaultIndex);
var client = new ElasticClient(settings);
if (client.IndexExists(defaultIndex).Exists)
client.DeleteIndex(defaultIndex);
var createIndexResponse = client.CreateIndex(defaultIndex, c => c
.Settings(s => s
.NumberOfShards(1)
.NumberOfReplicas(0)
)
.Mappings(m => m
.Map<MyEntity>(mm => mm
.AutoMap()
.Properties(p => p
.Keyword(k => k
.Name(n => n.RegionInfo)
)
)
)
)
);
var indexResponse = client.Index(new MyEntity
{
RegionInfo = RegionInfo.CurrentRegion
}, i => i.Refresh(Refresh.WaitFor));
}
public class MyEntity
{
public RegionInfo RegionInfo { get; set; }
}
public class RegionInfoConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value == null)
{
writer.WriteNull();
return;
}
writer.WriteValue(((RegionInfo)value).Name);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
if (reader.TokenType != JsonToken.String)
throw new JsonSerializationException($"Cannot deserialize {nameof(RegionInfo)} from {reader.TokenType}");
return new RegionInfo((string)reader.Value);
}
public override bool CanConvert(Type objectType)
{
return typeof(RegionInfo) == objectType;
}
}
The index request sends the following JSON
{
"regionInfo": "AU"
}

Derived types are not published to consumers in MassTransit

I am having issues publishing generic messages for derived types and having the handler invoked using MassTransit v2.8.0.
If I publish a message of type HtmlBlockNewMessage, the consumer is never invoked. If I publish a ServiceBusMessage object and change the consumer to Consumes<ServiceBusMessage>.Context, the consumer is invoked.
The code fails for a derived type. It only works for the parent type (ServiceBusMessage).
Types:
[Serializable]
public class ServiceBusMessage
{
}
[Serializable]
public class ServiceBusResponse
{
public int ResultCode { get; set; }
}
// Request
[Serializable]
public class ContentItemMessage : ServiceBusMessage
{
public string SiteId { get; set; }
public string PageId { get; set; }
public string ContentItemId { get; set; }
}
[Serializable]
public class HtmlBlockNewMessage : ContentItemMessage
{
public string HtmlData { get; set; }
}
// Response
[Serializable]
public class ContentItemMessageResponse : ServiceBusResponse
{
public string Name { get; set; }
public string ItemType { get; set; }
public string ItemHandler { get; set; }
}
[Serializable]
public class HtmlBlockNewMessageResponse : ContentItemMessageResponse
{
public string DataId { get; set; }
}
Consumer:
public class HtmlBlockConsumer : Consumes<HtmlBlockNewMessage>.Context
{
private IHtmlDataService htmlDataService;
public static ILogger Logger { get; set; }
public HtmlBlockConsumer()
: this(null)
{
}
public HtmlBlockConsumer(IHtmlDataService htmlDataService)
{
Logger = Log4NetLogger.GetLogger();
this.htmlDataService = htmlDataService ?? IoC.Container.Resolve<IHtmlDataService>();
}
public void Consume(IConsumeContext<HtmlBlockNewMessage> message)
{
// Do some stuff
message.Respond(new HtmlBlockNewMessageResponse() { ResultCode = 1 } );
}
}
Bus registration from publisher side:
var bus = ServiceBusFactory.New(sbc =>
{
sbc.EnableMessageTracing();
sbc.UseMsmq();
sbc.VerifyMsmqConfiguration();
sbc.UseMulticastSubscriptionClient();
sbc.SetNetwork("Test");
sbc.UseXmlSerializer();
sbc.UseControlBus();
sbc.ReceiveFrom("msmq://localhost/AuctionCMS.Web.Publisher");
MtServiceBus.ValidateBus(sbc);
});
IoC.Container.RegisterInstance(bus);
Bus registration from consumer side:
var bus = ServiceBusFactory.New(sbc =>
{
sbc.EnableMessageTracing();
sbc.UseMsmq();
sbc.VerifyMsmqConfiguration();
sbc.UseMulticastSubscriptionClient();
sbc.SetNetwork("Test");
sbc.UseXmlSerializer();
sbc.UseControlBus();
sbc.ReceiveFrom("msmq://localhost/AuctionCMS.Consumer");
sbc.Subscribe(subs =>
{
// These are being manually registered due to some issues getting
// StructureMap to scan my assemblies
subs.Instance(new HtmlBlockConsumer());
subs.Instance(new BrowserConsumer());
subs.Instance(new OfferConsumer());
});
});
IoC.Container.RegisterInstance(bus);
My publish extension:
public static TR Publish<T, TR>(this IServiceBus bus, T message) where T : ServiceBusMessage where TR : ServiceBusResponse
{
TR response = null;
IoC.Container.Resolve<IServiceBus>().PublishRequest(message, callback =>
{
callback.SetTimeout(10.Seconds());
try
{
callback.Handle<TR>(m =>
{
response = m; /
});
}
catch (Exception ex)
{
throw;
}
});
return response;
}
Calling code:
// First I create a message specific to the type of action I am performing
var message = new HtmlBlockNewMessage() { HtmlData = "Hello" };
// Then I call a function which accepts a ContentItemMessage and calls Publish
public void AddContentItem(ContentItemMessage message)
{
// Do some preprocessing
// This call times out
var response = this.auctionCmsServices.Bus.Publish<ContentItemMessage,
ContentItemMessageResponse>(message);
// Do some more processing
}
This is the exception
[RequestTimeoutException: Timeout waiting for response, RequestId: 54910000-307f-20cf-c0c2-08d06b31cf6f]
MassTransit.RequestResponse.RequestImpl`1.Wait() in d:\BuildAgent-03\work\aa063b4295dfc097\src\MassTransit\RequestResponse\RequestImpl.cs:124
MassTransit.RequestResponseExtensions.PublishRequest(IServiceBus bus, TRequest message, Action`1 configureCallback) in d:\BuildAgent-03\work\aa063b4295dfc097\src\MassTransit\RequestResponseExtensions.cs:31
AuctionCMS.Framework.ServiceBus.MtServiceBus.Publish(IServiceBus bus, T message) in c:\Users\rick\Documents\Visual Studio 2012\Projects\AuctionCMS\AuctionCMS.Framework\ServiceBus\MtServiceBus.cs:24
AuctionCMS.Framework.Entity.Page.AddContentItem(ISite site, String zone, Int32 location, ContentItemMessage message) in c:\Users\rick\Documents\Visual Studio 2012\Projects\AuctionCMS\AuctionCMS.Framework\Entity\Page.cs:48
AuctionCMS.Framework.Entity.Site.SetDefaultContent() in c:\Users\rick\Documents\Visual Studio 2012\Projects\AuctionCMS\AuctionCMS.Framework\Entity\Site.cs:117
AuctionCMS.Web.Controllers.AdminSitesController.NewSite(SiteNewModel model, HttpPostedFileBase file) in c:\Users\rick\Documents\Visual Studio 2012\Projects\AuctionCMS\AuctionCMS.Web\Controllers\AdminSitesController.cs:69
lambda_method(Closure , ControllerBase , Object[] ) +179
System.Web.Mvc.ReflectedActionDescriptor.Execute(ControllerContext controllerContext, IDictionary`2 parameters) +261
System.Web.Mvc.ControllerActionInvoker.InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary`2 parameters) +39
System.Web.Mvc.Async.<>c__DisplayClass42.<BeginInvokeSynchronousActionMethod>b__41() +34
System.Web.Mvc.Async.<>c__DisplayClass39.<BeginInvokeActionMethodWithFilters>b__33() +124
System.Web.Mvc.Async.<>c__DisplayClass4f.<InvokeActionMethodFilterAsynchronously>b__49() +838059
System.Web.Mvc.Async.<>c__DisplayClass4f.<InvokeActionMethodFilterAsynchronously>b__49() +838059
System.Web.Mvc.Async.<>c__DisplayClass4f.<InvokeActionMethodFilterAsynchronously>b__49() +838059
System.Web.Mvc.Async.<>c__DisplayClass4f.<InvokeActionMethodFilterAsynchronously>b__49() +838059
System.Web.Mvc.Async.<>c__DisplayClass37.<BeginInvokeActionMethodWithFilters>b__36(IAsyncResult asyncResult) +15
System.Web.Mvc.Async.<>c__DisplayClass2a.<BeginInvokeAction>b__20() +33
System.Web.Mvc.Async.<>c__DisplayClass25.<BeginInvokeAction>b__22(IAsyncResult asyncResult) +838644
System.Web.Mvc.<>c__DisplayClass1d.<BeginExecuteCore>b__18(IAsyncResult asyncResult) +28
System.Web.Mvc.Async.<>c__DisplayClass4.<MakeVoidDelegate>b__3(IAsyncResult ar) +15
System.Web.Mvc.Controller.EndExecuteCore(IAsyncResult asyncResult) +65
System.Web.Mvc.Async.<>c__DisplayClass4.<MakeVoidDelegate>b__3(IAsyncResult ar) +15
System.Web.Mvc.Controller.EndExecute(IAsyncResult asyncResult) +51
System.Web.Mvc.<>c__DisplayClass8.<BeginProcessRequest>b__3(IAsyncResult asyncResult) +42
System.Web.Mvc.Async.<>c__DisplayClass4.<MakeVoidDelegate>b__3(IAsyncResult ar) +15
System.Web.Mvc.MvcHandler.EndProcessRequest(IAsyncResult asyncResult) +51
System.Web.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +606
System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +288
Edit:
I went with a generic approach to solve this. It's a but ugly from the caller's perspective, but it works.
public TR AddContentItem<T, TR>(T message) where T : ContentItemMessage where TR : ContentItemMessageResponse
{
var response = this.auctionCmsServices.Bus.Publish<T, TR>(message);
return response;
}
The calling code now looks like this:
page.AddContentItem(new HtmlBlockNewMessage() { HtmlData =
"This is some html" });
Eugene's comment is correct. What's happening here is you are publishing a message of type ContentItemMessage. A consumer of HtmlBlockNewMessage will not executed since the message is published as a ContentItemMessage and a ServiceBusMessage. MassTransit message mis-typing is one of a number of things out there on how this works.
Your options:
Change AddContentItem to use a generic, perhaps with a constraint
Used reflection to invoke Publish with the right type information
Restructure how you publish things so this isn't an issue any more
The bottom line is you should always publish as the type you want received. Polymorphism in messaging is tricky.

Optional Dictionary Parameter in MVC 3 Controller Action

I had an MVC 3 controller action defined like this:
public ActionResult _UpdateFilter(int a, string b, List<string> c)
that worked great. There's a new requirement to sometimes also send a Dictionary<int, bool> to the action, depending on user action in the UI.
To populate that dictionary, I add hidden fields to the appropriate form like this:
<input type="hidden" id="id-#(something)-Key" name="sometimesSet[#(idx)].Key" value="#(myIntValue)" />
<input type="hidden" id="id-#(something)-Value" name="sometimesSet[#(idx)].Value" value="#(myBoolValue)" />
and I extended my Controller Action like this:
public ActionResult _UpdateFilter(int a, string b, List<string> c, Dictionary<int, bool> sometimesSet = null)
So far, so good. As long as I populate at least one hidden key/value pair in the form. If I do not, my Action is not invoked and I get the exception:
Value cannot be null.
Parameter name: key
(long stack trace at end of question).
The Question
I gather the route mapping can't figure out that sometimesSet is optional, but I don't know how to configure that properly. How do I do that? I have not edited the route definitions in Global.asax.
The stack trace
at System.Collections.Generic.Dictionary`2.FindEntry(TKey key)
at System.Collections.Generic.Dictionary`2.ContainsKey(TKey key)
at System.Web.Mvc.ValueProviderUtil.GetKeysFromPrefix(IEnumerable`1 collection, String prefix)
at System.Web.Mvc.DictionaryValueProvider`1.GetKeysFromPrefix(String prefix)
at System.Web.Mvc.ValueProviderCollection.GetKeysFromPrefixFromProvider(IValueProvider provider, String prefix)
at System.Web.Mvc.ValueProviderCollection.<>c__DisplayClass11.<GetKeysFromPrefix>b__c(IValueProvider provider)
at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
at System.Linq.Enumerable.FirstOrDefault[TSource](IEnumerable`1 source)
at System.Web.Mvc.ValueProviderCollection.GetKeysFromPrefix(String prefix)
at System.Web.Mvc.DefaultModelBinder.UpdateDictionary(ControllerContext controllerContext, ModelBindingContext bindingContext, Type keyType, Type valueType)
at System.Web.Mvc.DefaultModelBinder.BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
at System.Web.Mvc.DefaultModelBinder.BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
at System.Web.Mvc.ControllerActionInvoker.GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor)
at System.Web.Mvc.ControllerActionInvoker.GetParameterValues(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
at System.Web.Mvc.Async.AsyncControllerActionInvoker.<>c__DisplayClass25.<BeginInvokeAction>b__1e(AsyncCallback asyncCallback, Object asyncState)
at System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncResult`1.Begin(AsyncCallback callback, Object state, Int32 timeout)
at System.Web.Mvc.Async.AsyncControllerActionInvoker.BeginInvokeAction(ControllerContext controllerContext, String actionName, AsyncCallback callback, Object state)
at System.Web.Mvc.Controller.<>c__DisplayClass1d.<BeginExecuteCore>b__17(AsyncCallback asyncCallback, Object asyncState)
at System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncResult`1.Begin(AsyncCallback callback, Object state, Int32 timeout)
at System.Web.Mvc.Controller.BeginExecuteCore(AsyncCallback callback, Object state)
at System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncResult`1.Begin(AsyncCallback callback, Object state, Int32 timeout)
at System.Web.Mvc.Controller.BeginExecute(RequestContext requestContext, AsyncCallback callback, Object state)
at System.Web.Mvc.Controller.System.Web.Mvc.Async.IAsyncController.BeginExecute(RequestContext requestContext, AsyncCallback callback, Object state)
at System.Web.Mvc.MvcHandler.<>c__DisplayClass6.<>c__DisplayClassb.<BeginProcessRequest>b__3(AsyncCallback asyncCallback, Object asyncState)
at System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncResult`1.Begin(AsyncCallback callback, Object state, Int32 timeout)
at System.Web.Mvc.MvcHandler.<>c__DisplayClass6.<BeginProcessRequest>b__2()
at System.Web.Mvc.SecurityUtil.<>c__DisplayClassb`1.<ProcessInApplicationTrust>b__a()
at System.Web.Mvc.SecurityUtil.<GetCallInAppTrustThunk>b__0(Action f)
at System.Web.Mvc.SecurityUtil.ProcessInApplicationTrust(Action action)
at System.Web.Mvc.SecurityUtil.ProcessInApplicationTrust[TResult](Func`1 func)
at System.Web.Mvc.MvcHandler.BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, Object state)
at System.Web.Mvc.MvcHandler.BeginProcessRequest(HttpContext httpContext, AsyncCallback callback, Object state)
at System.Web.Mvc.MvcHandler.System.Web.IHttpAsyncHandler.BeginProcessRequest(HttpContext context, AsyncCallback cb, Object extraData)
at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)
I found the cleanest solution is to implement my own ModelBinder that understands how to populate a Dictionary<K,V> even if no relevant values are found on the wire (it will return an empty Dictionary<K,V>.
Here's the code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace My.ModelBinders
{
/// <summary>
/// Binds to a generic Dictionary using the basic format outlined at
/// http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx
/// with the relaxation that the indices can be arbitrary integers (need not start at 0 or be sequential).
/// Returns an empty dictionary of no matching parameters found on the wire rather than throwing
/// an Exception as the current default binder does.
/// </summary>
/// <typeparam name="K">Key type</typeparam>
/// <typeparam name="V">Value type</typeparam>
public class OptionalDictionaryBinder<K,V> : CustomBinderBase, IModelBinder
{
/// <summary>
/// Pull key/value pairs out of request. Modified from
/// https://github.com/loune/MVCStuff/blob/master/Extensions/DefaultDictionaryBinder.cs
/// Files not currently supported.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private IEnumerable<KeyValuePair<string, string>> GetValueProviderData(ControllerContext context)
{
foreach (var fk in context.HttpContext.Request.Form.Keys)
{
yield return new KeyValuePair<string, string>((string)fk, context.HttpContext.Request.Form[(string)fk]);
}
foreach (var rd in context.RouteData.Values)
{
yield return new KeyValuePair<string, string>(rd.Key, (string)rd.Value);
}
foreach (var qp in context.HttpContext.Request.QueryString)
{
yield return new KeyValuePair<string, string>((string)qp, context.HttpContext.Request.QueryString[(string)qp]);
}
}
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (bindingContext == null)
{
throw new ArgumentNullException("bindingContext");
}
Dictionary<K, V> dict = new Dictionary<K, V>();
Dictionary<int, K> keys = new Dictionary<int, K>();
Dictionary<int, V> values = new Dictionary<int, V>();
foreach (KeyValuePair<string,string> formParam in GetValueProviderData(controllerContext))
{
if (formParam.Key.StartsWith(bindingContext.ModelName, StringComparison.InvariantCultureIgnoreCase))
{
int startbracket = formParam.Key.IndexOf("[");
if (startbracket != bindingContext.ModelName.Length) throw new Exception("Did not find [ directly after model name");
int endbracket = formParam.Key.IndexOf("]", bindingContext.ModelName.Length + 1);
if (endbracket == -1) throw new Exception("Did not find closing bracket in " + formParam);
int idx;
string idxText = formParam.Key.Substring(bindingContext.ModelName.Length + 1, endbracket - bindingContext.ModelName.Length - 1);
if (!int.TryParse(idxText, out idx))
{
throw new Exception("Could not parse numeric index from " + formParam);
}
if (formParam.Key.EndsWith(".Key", StringComparison.InvariantCultureIgnoreCase))
{
if (keys.ContainsKey(idx))
{
throw new Exception("The index " + idx + " was repeated.");
}
K dictKey;
try
{
dictKey = (K)Convert.ChangeType(formParam.Value, typeof(K));
keys.Add(idx, dictKey);
}
catch (NotSupportedException)
{
throw new Exception("The given key '" + formParam.Key + "' could not be converted to type K");
}
}
else if (formParam.Key.EndsWith(".Value", StringComparison.InvariantCultureIgnoreCase))
{
if (values.ContainsKey(idx))
{
throw new Exception("The index " + idx + " was repeated.");
}
V dictValue;
try
{
dictValue = (V)Convert.ChangeType(formParam.Value, typeof(V));
values.Add(idx, dictValue);
}
catch (NotSupportedException)
{
throw new Exception("The given value '" + formParam.Value + "' could not be converted to type V");
}
}
}
}
if (!keys.Keys.SequenceEqual(values.Keys))
{
throw new Exception("Keys and values do not match.");
}
foreach (KeyValuePair<int, K> kvp in keys)
{
dict.Add(kvp.Value, values[kvp.Key]);
}
return dict;
}
}
}
Usage:
public ActionResult _UpdateFilter(int a, string b, List<string> c,
[ModelBinder(typeof(OptionalDictionaryBinder<int, bool>))]
Dictionary<int, bool> sometimesSet)

Resources