Unit testing using Fakes gives IConvertible exception - microsoft-fakes

What I have
The interface:
public interface IDocDbRepository
{
Task<JObject> UpdateDocumentAsync(JObject updatedDocument);
}
The implementation:
public async Task<JObject> UpdateDocumentAsync(JObject updatedDocument)
{
if (updatedDocument == null)
{
throw new ArgumentNullException(nameof(updatedDocument));
}
var response = await this.documentDBClient.ReplaceDocumentAsync(UriFactory.CreateDocumentUri(this.dbName, this.collectionName, updatedDocument["id"].Value<string>()), updatedDocument).ConfigureAwait(false);
return JObject.Parse(response.Resource.ToString());
}
The exception occurs in the await line.
The Unit Test:
static Guid docGuid = Guid.NewGuid();
[TestMethod]
public async Task TestMethod2()
{
var jObject = new JObject { { "id", docGuid }, { "studentId", "1" }, { "courseId", "Ph" } };
// Arrange
var docClient = new ShimDocumentClient();
ShimDocumentClient.AllInstances.CreateDocumentAsyncUriObjectRequestOptionsBoolean =
(instance, uri, document, options, disableAutomaticGeneration) => Task.FromResult(new ResourceResponse<Document>(new Document() { Id = docGuid.ToString() }));
// Act
var documentRepository = new DocDbRepository(endPointUri, accountKey, dbName, collectionName);
try{
var response = await documentRepository.UpdateDocumentAsync(jObject).ConfigureAwait(false);
}
catch(Exception ex){}
// Assert
Assert.AreEqual(response.Count, 1);
}
The test does not go beyond the UpdateDocumentAsync part and exits with this message:
at System.Convert.ChangeType(Object value, Type conversionType, IFormatProvider provider)
at Newtonsoft.Json.Linq.Extensions.Convert[T,U](T token)
at Newtonsoft.Json.Linq.Extensions.Value[T,U](IEnumerable`1 value)
at Newtonsoft.Json.Linq.Extensions.Value[U](IEnumerable`1 value)
at Common.DataAccess.DocumentDb.DocDbRepository.<UpdateDocumentAsync>d__12.MoveNext() in C:\Common\Common.DataAccess.DocumentDb\DocDbRepository.cs:line 196
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)
at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
at Common.DataAccess.DocumentDb.Tests.DocDbUtilityTests.<TestMethod2>d__9.MoveNext() in C:\Common\Common.DataAccess.DocumentDb.Tests\DocDbUtilityTests.cs:line 113
This is my first time with Fakes framework.
Any help is greatly appreciated.
Thanks in advance.
Regards.

This appears to be a problem with your serialization code. Specifically, this statement:
updatedDocument["id"].Value<string>()
The Value extension method appears to require that the source implement the IConvertible interface, which is not implemented by Guid.

Related

MassTransit with AWS SQS - exception sending dynamic message

I'm using Masstransit with AWS SQS/SNS as a transport.
Now I'm stuck with a simple problem - adding CorrelationId to published message. Since I'm following recommendation to use interfaces as DTOs, so I can't use CorrelatedBy<Guid> interface and I decided to add __CorrelationId property to messages directly.
I'm using following wrapper to publish message:
public class MessageBus : IMessageBus
{
// ... omitted for brevity
public async Task Publish<T>(object message)
where T : class
{
await this.publishEndpoint.Publish<T>(this.CreateCorrelatedMessage(message));
}
private object CreateCorrelatedMessage(object message)
{
dynamic corellatedMessage = new ExpandoObject();
foreach (var property in message.GetType().GetProperties())
{
((IDictionary<string, object>)corellatedMessage).Add(property.Name, property.GetValue(message));
}
corellatedMessage.__CorrelationId = this.correlationIdProvider.CorrelationId;
return corellatedMessage;
}
}
And here is the usage:
await this.messageBus.Publish<ObtainRequestToken>(new
{
Social = SocialEnum.Twitter,
AppId = credentials.First(x => x.Name == "TwitterConsumerKey").Value,
AppSecret = credentials.First(x => x.Name == "TwitterConsumerSecret").Value,
ReturnUrl = returnUrl
});
This works well in console app with following bus registration:
var busControl = Bus.Factory.CreateUsingAmazonSqs(cfg =>
{
cfg.Host("eu-west-1", h =>
{
h.AccessKey("xxx");
h.SecretKey("xxx");
h.Scope($"Local", scopeTopics: true);
});
});
But is ASP.NET Core application with
services.AddMassTransit(cfg =>
{
cfg.UsingAmazonSqs((context, sqsCfg) =>
{
var amazonAccount = this.Configuration
.GetSection("AmazonAccount")
.Get<AmazonAccountConfig>();
sqsCfg.Host(amazonAccount.Region, h =>
{
h.AccessKey(amazonAccount.KeyId);
h.SecretKey(amazonAccount.SecretKey);
h.Scope(this.Environment.EnvironmentName, scopeTopics: true);
});
});
cfg.SetEndpointNameFormatter(new DefaultEndpointNameFormatter(this.Environment.EnvironmentName + "_", false));
});
services.AddMassTransitHostedService();
It fails on publish with Object reference not set to an instance of an object. and stacktrace
at MassTransit.Logging.EnabledDiagnosticSource.StartSendActivity[T](SendContext1 context, ValueTuple2[] tags)
at MassTransit.AmazonSqsTransport.Transport.TopicSendTransport.SendPipe1.<Send>d__5.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.ConfiguredTaskAwaitable.ConfiguredTaskAwaiter.GetResult()
at GreenPipes.Agents.PipeContextSupervisor1.<GreenPipes-IPipeContextSource<TContext>-Send>d__7.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at GreenPipes.Agents.PipeContextSupervisor1.<GreenPipes-IPipeContextSource<TContext>-Send>d__7.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at GreenPipes.Agents.PipeContextSupervisor1.<GreenPipes-IPipeContextSource<TContext>-Send>d__7.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.ConfiguredTaskAwaitable.ConfiguredTaskAwaiter.GetResult()
at MassTransit.Initializers.MessageInitializer2.<Send>d__9.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.ConfiguredTaskAwaitable.ConfiguredTaskAwaiter.GetResult()
at MassTransit.Transports.PublishEndpoint.<>c__DisplayClass18_01.<<PublishInternal>g__PublishAsync|0>d.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Dynamic.UpdateDelegates.UpdateAndExecuteVoid1[T0](CallSite site, T0 arg0)
at xxx.Main.Api.Messaging.MessageBus.<Publish>d__51.MoveNext() in D:\repo\projects\xxx\mainapi\xxx.Main.Api\Messaging\MessageBus.cs:line 34
StartSendActivity looks like simple diagnostics method and I can't fugure out, what can be failing there.
You can't send object or other types like that with MassTransit. Messages must be reference types.
Relevant Documentation

Application Insights RequestTelemetry not showing up in Requests after exception

I have spent a while trying to RequestTelemetry to work. It did when I was first playing around with it, but then oddly just stopped working whenever an exception is thrown. I have read documentation using Application Insights for custom events and metrics as well as Custom Operations Tracking and tried to add all of the best practices to see if I could get the result to show up again. I'm using .NET Core 3.1 and Microsoft.ApplicationInsights.AspNetCore 2.14.0.
Setup for the Webapp looks like this in Startup.cs
services.AddApplicationInsightsTelemetry(new ApplicationInsightsServiceOptions {
EnableAdaptiveSampling = false
});
I have the telemetry inside of a Controller Post Action. I realize that Application Insights is already tracking it the post action, but I wanted to see if I could track the inner method. This is the code in my controller:
public MyController(IMyService myService, TelemetryClient telemetryClient, ILogger<MyController> logger) {
_myService = myService;
_telemetryClient = telemetryClient;
_logger = logger;
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> PostAsync([FromBody] MyModel model) {
using var scope = _logger.BeginScope(new Dictionary<string, object> {
{ $"{nameof(PostAsync)}.Scope", Guid.NewGuid() },
{ nameof(model.Name), model.Name }
});
model.AuthenticatedUserId = User.GetUserIdFromClaims();
var requestTelemetry = new RequestTelemetry { Name = nameof( _myService.MyFunctionAsync) };
var operation = _telemetryClient.StartOperation(requestTelemetry);
operation.Telemetry.Properties.Add("User", model.AuthenticatedUserId);
try {
await _myService.MyFunctionAsync(model).ConfigureAwait(false); // <-- throws exception
operation.Telemetry.Success = true;
return NoContent();
} catch (Exception e) {
operation.Telemetry.Success = false;
throw;
} finally {
_telemetryClient.StopOperation(operation);
}
}
I can see in the Visual Studio console output that the code executes, as I get the following log, but it never shows up in the Application Insights Requests.
Application Insights Telemetry: {
"name": "AppRequests",
"time": "2020-06-21T14:29:08.7469588Z",
"iKey": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"tags": {
"ai.application.ver": "1.0.0.0",
"ai.cloud.roleInstance": "DESKTOP-K74PNCU",
"ai.operation.id": "0443259d660125498cf28f8f7a275dab",
"ai.operation.parentId": "1dea6f9b27220c4c",
"ai.operation.name": "POST EventEmitter/Post",
"ai.location.ip": "::1",
"ai.internal.sdkVersion": "dotnetc:2.14.0-17971",
"ai.internal.nodeName": "DESKTOP-K74PNCU"
},
"data": {
"baseType": "RequestData",
"baseData": {
"ver": 2,
"id": "2b7900eedfb7c34d",
"name": "MyFunctionAsync",
"duration": "00:00:00.3766937",
"success": false,
"properties": {
"DeveloperMode": "true",
"User": "pobl-dev",
"_MS.ProcessedByMetricExtractors": "(Name:'Requests', Ver:'1.1')",
"AspNetCoreEnvironment": "Development"
}
}
}
}
There is a simple solution, but I'm not sure of why it's necessary, due to either a lack in documentation or a bug. I found once a responseCode was provided everything works fine. There is a default responseCode of 200 which shows up on a successful call. Once I set the value on a failure everything worked fine.
public MyController(IMyService myService, TelemetryClient telemetryClient, ILogger<MyController> logger) {
_myService = myService;
_telemetryClient = telemetryClient;
_logger = logger;
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> PostAsync([FromBody] MyModel model) {
using var scope = _logger.BeginScope(new Dictionary<string, object> {
{ $"{nameof(PostAsync)}.Scope", Guid.NewGuid() },
{ nameof(model.Name), model.Name }
});
model.AuthenticatedUserId = User.GetUserIdFromClaims();
var requestTelemetry = new RequestTelemetry { Name = nameof( _myService.MyFunctionAsync) };
var operation = _telemetryClient.StartOperation(requestTelemetry);
operation.Telemetry.Properties.Add("User", model.AuthenticatedUserId);
try {
await _myService.MyFunctionAsync(model).ConfigureAwait(false); // <-- throws exception
operation.Telemetry.Success = true;
operation.Telemetry.ResponseCode = "Roses";
return NoContent();
} catch (Exception e) {
operation.Telemetry.Success = false;
operation.Telemetry.ResponseCode = "Funky"; // <-- seems to be required on a failure
throw;
} finally {
_telemetryClient.StopOperation(operation);
}
}
This is to add some context to the accepted answer if you're curious:
Here's the source code for RequestTelemetry
When it prepares the data to send to Azure servers it explicitly elects NOT to set a default response code unless success == true in which case the default is 200.
// Required fields
if (!this.Success.HasValue)
{
this.Success = true;
}
if (string.IsNullOrEmpty(this.ResponseCode))
{
this.ResponseCode = this.Success.Value ? "200" : string.Empty;
}
If you run a simple Kusto query on the logs:
union requests
| where timestamp > ago(1hr)
| where customDimensions["CustomOperationCategory"] in ("Identity")
| take 100
You'll only see unsuccessful results where you did set a status code:
I don't know if something ever changed, but Microsoft's examples sometimes do the same.

Unexpected end of stream at Microsoft.AspNetCore.WebUtilities.MultipartReaderStream

System.IO.IOException: Unexpected end of stream.
at Microsoft.AspNetCore.WebUtilities.MultipartReaderStream.<ReadAsync>d__32.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Microsoft.AspNetCore.WebUtilities.StreamHelperExtensions.<DrainAsync>d__2.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at Microsoft.AspNetCore.WebUtilities.MultipartReader.<ReadNextSectionAsync>d__14.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at AspNetCoreFileUpload.Controllers.FileUploadController.<Index>d__0.MoveNext()
in C:\\GitHub\\StackOverflow\\LargeFileUploadController\\FileUploadController.cs:line 29
Repro: https://github.com/bigfont/StackOverflow/tree/master/LargeFileUploadController
Form
<form action = ""/FileUpload"" method=""post"" enctype=""multipart/form-data"">
<label for=""myfile1"">File</label>
<input type=""file"" name=""myFile1"" />
<label for=""myfile2"">File</label>
<input type=""file"" name=""myFile2"" />
<input type=""submit"" value=""Send"" />
</form>
Controller
public class FileUploadController : Controller
{
[HttpPost]
public async Task<IActionResult> Index()
{
var boundary = GetBoundary(Request.ContentType);
var reader = new MultipartReader(boundary, Request.Body);
try
{
var section = await reader.ReadNextSectionAsync();
}
catch (System.Exception ex)
{
return new OkObjectResult(new { ex = ex.ToString() });
}
return new OkObjectResult(new { message = "Done" });
}
private static string GetBoundary(string contentType)
{
var elements = contentType.Split(' ');
var element = elements.Where(entry => entry.StartsWith("boundary=")).First();
var boundary = element.Substring("boundary=".Length);
// Remove quotes
if (boundary.Length >= 2 &&
boundary[0] == '"' && boundary[boundary.Length - 1] == '"')
{
boundary = boundary.Substring(1, boundary.Length - 2);
}
return boundary;
}
}
I got almost the same exception recently. I'm saying almost because they actually renamed the exception to Unexpected end of Stream, the content may have already been read by another component., which actually means that something already consumed the body stream. The comments of the following change gives us the understanding of what's happening:
Tratcher commented on Mar 23
...The MVC model binder reads the form and buffers the multipart segments
for you, so there's no point in re-parsing request body with the
MultipartReader...
So, the question is how to disable the default form binding (reading the request form)?
I found the DisableFormValueModelBindingAttribute attribute in this Mvc.FileUpload sample which disables the form binding and this is what it looks like:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var formValueProviderFactory = context.ValueProviderFactories
.OfType<FormValueProviderFactory>()
.FirstOrDefault();
if (formValueProviderFactory != null)
{
context.ValueProviderFactories.Remove(formValueProviderFactory);
}
var jqueryFormValueProviderFactory = context.ValueProviderFactories
.OfType<JQueryFormValueProviderFactory>()
.FirstOrDefault();
if (jqueryFormValueProviderFactory != null)
{
context.ValueProviderFactories.Remove(jqueryFormValueProviderFactory);
}
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
If you want some more information, you can check out the following:
Create a filter/sample that shows how to remove the form value providers (rynowak opened this issue on Apr 26)
Sample: Issues with Antiforgery + Form + File Uploads (rynowak opened this issue on Apr 26)
Just for info - as commented before, the MVC model binder reads the form, but where can one find the results. The results can be found in the HttpRequest.Form, which has Files.
Don't know if this might help you but I came across simular issue "Unexpected end of Stream, the content may have already been read by another component".
app.Use(async (context, next) => {
context.Request.EnableRewind();
await next();
});
Code above were added in Startup.cs Configure method.
Hope it helps
I created a MemoryStream, copied stream from body there and it was working like a charm :) Point is you cannot read Stream twice. However, this is not a case for MemoryStream. Of course, you have to be sure about scaling, I don't think this will work for really big files uploaded. I didn't tested this.
I rewrote example from Microsoft site: enter link description here
Here is part of it:
while (section != null)
{
ContentDispositionHeaderValue contentDisposition;
var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out contentDisposition);
if (hasContentDispositionHeader)
{
if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
{
var ms = new MemoryStream();
var fileSection = section.AsFileSection();
await fileSection.FileStream.CopyToAsync(ms);
ms.Position = 0;
documentUpload.Attachments.Add(new SimpleFileInstance { FileName = fileSection.FileName, FileStream = ms });
}
else if (MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition))
{
// Content-Disposition: form-data; name="key"//
// value
// Do not limit the key name length here because the
// multipart headers length limit is already in effect.
var key = HeaderUtilities.RemoveQuotes(contentDisposition.Name).Value;
var encoding = GetEncoding(section);
using (var streamReader = new StreamReader(
section.Body,
encoding,
detectEncodingFromByteOrderMarks: true,
bufferSize: 1024,
leaveOpen: true))
{
// The value length limit is enforced by MultipartBodyLengthLimit
var value = await streamReader.ReadToEndAsync();
if (string.Equals(value, "undefined", StringComparison.OrdinalIgnoreCase))
{
value = string.Empty;
}
formAccumulator.Append(key, value);
if (formAccumulator.ValueCount > DefaultFormOptions.ValueCountLimit)
{
throw new InvalidDataException($"Form key count limit {DefaultFormOptions.ValueCountLimit} exceeded.");
}
}
}
}
section = await reader.ReadNextSectionAsync();
}
documentUpload is our DTO to work further with files. In our case, some documents gets uploaded to SharePoint.

How to create a SecondaryTile on Windows Phone 8.1?

I'm trying to create a Tile on the Windows Phone start screen. The following code used to work but now it doesn't. I didn't change anything.
private async static void CreateTile()
{
try
{
SecondaryTile tileData = new SecondaryTile()
{
TileId = "MyTileID",
DisplayName = "My App Name",
TileOptions = TileOptions.ShowNameOnLogo,
};
tileData.VisualElements.Square150x150Logo = new Uri("ms-appx:///Resources/Images/Tiles/150150.png", UriKind.Absolute);
tileData.VisualElements.ShowNameOnSquare150x150Logo = true;
await tileData.RequestCreateAsync();
}
catch (Exception ex)
{
}
}
Now it's failing with the error message:
The parameter is incorrect.
and the following Stack Trace:
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at My.Namespace.SplashPage.<CreateTile>d__f.MoveNext()
I even get the error when I comment everything out, for example:
SecondaryTile tileData = new SecondaryTile()
{
//TileId = "MyTileID",
//DisplayName = "My App Name",
//TileOptions = TileOptions.ShowNameOnLogo,
};
//tileData.VisualElements.Square150x150Logo = new Uri("ms-appx:///Resources/Images/Tiles/150150.png", UriKind.Absolute);
//tileData.VisualElements.ShowNameOnSquare150x150Logo = true;
await tileData.RequestCreateAsync();
So I don't know what parameter is incorrect. What could be causing this? How can I fix it?
Try something like this:
if (SecondaryTile.Exists(tileID)) await tileData.UpdateAsync();
else await tileData.RequestCreateAsync();

How do I log EntityValidation errors using ELMAH MVC?

I've been writing an application using MVC4 and EF5.x, and using ELMAH for logging exceptions for review. We recently released the application, and as expected the ELMAH log filled up with several dozen exceptions. Great (and not)! The problem is that one of those exceptions is
System.Data.Entity.Validation.DbEntityValidationException
Validation failed for one or more entities.
See 'EntityValidationErrors' property for more details.
Of course, there's no way to see the EntityValidationErrors property for more details and the stack trace wraps up to my SubmitChanges()
I know ELMAH has the capability of allowing us to raise our own exceptions, and in some way customize what gets logged and how. Unfortunately, I'm still very new to ELMAH and MVC and a Google search didn't turn up anything relevant. I did find a blog article on logging EntityValidationErrors, and the author specifically mentioned that he would post how to do so in ELMAH but that was posted in September of 2012 and I didn't see anything since then.
Any help would be greatly appreciated!
Probably the best thing to do in this case would be to wrap your context.SaveChanges(); call in a try...catch block and then log the individual items from the ValidationExceptions. Something like the following should get you started:
try
{
context.SaveChanges();
}
catch (DbEntityValidationException ve)
{
var error = ve.EntityValidationErrors.First().ValidationErrors.First();
var msg = String.Format("Validation Error :: {0} - {1}",
error.PropertyName, error.ErrorMessage);
var elmahException = new Exception(msg);
Elmah.ErrorSignal.FromCurrentContext().Raise(elmahException);
}
How about this extension method based on the above..
public static void SaveChangesWithBetterValidityException(this DbContext context)
{
try
{
context.SaveChanges();
}
catch (DbEntityValidationException ve)
{
var errors = new List<string>();
foreach (var e in ve.EntityValidationErrors)
{
errors.AddRange(e.ValidationErrors.Select(e2 => string.Join("Validation Error :: ", e2.PropertyName, " : ", e2.ErrorMessage)));
}
var error = string.Join("\r\n", errors);
var betterException = new Exception(error, ve);
throw betterException;
}
}
Elmah will then have a much better exception in it's log
I added the following to my Global.asax.cs in order to forward all DbEntityValidationException exceptions to Elmah across my MVC application:
private void ElmahEntityValidationException()
{
var dbEntityValidationException = Server.GetLastError() as DbEntityValidationException;
if (dbEntityValidationException != null)
{
var errors = new List<string>();
foreach (var entityError in dbEntityValidationException.EntityValidationErrors)
{
errors.AddRange(entityError.ValidationErrors.Select(e2 => string.Join("Validation Error :: ", e2.PropertyName, " : ", e2.ErrorMessage)));
}
var error = string.Join("\r\n", errors);
var betterException = new Exception(error, dbEntityValidationException);
Elmah.ErrorSignal.FromCurrentContext().Raise(betterException);
}
}
protected void Application_Error(object sender, EventArgs e)
{
ElmahEntityValidationException();
}
Some of this code was reused from #Paige Cook's and #Original10's posts.
Re-throwing as per the code below is not perfect (although I don't mind resetting the call stack here, as Elmah's logged details of the address posted to will show me what lead to the exception) and you will have to work out your own security implications, but this is fairly concise & meets my needs:
try
{
return base.SaveChanges();
}
catch (DbEntityValidationException e)
{
var de = new DetailedEntityValidationException(e);
throw de;
}
public class DetailedEntityValidationException : Exception
{
public DetailedEntityValidationException(DbEntityValidationException ve)
: base(ve.Message + ":\r\n\t-" + string.Join(new string('-',20) + "\r\n\t-", ve.EntityValidationErrors.Select(ev=>string.Join("\r\n\t-",ev.ValidationErrors.Select(e=>e.ErrorMessage)))))
{}
}
Here is my implementation for Global Web API solution for Elmah and EF Validation errors:
public class ElmahHandleWebApiErrorAttribute : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
var e = context.Exception;
// Try parse as entity error (i'm not sure of performance implications here)
var efValidationError = e as DbEntityValidationException;
if (efValidationError == null)
{
RaiseErrorSignal(e);
}
else
{
RaiseEntityFrameWorkValidationErrorSignal(efValidationError);
}
}
private static bool RaiseErrorSignal(Exception e)
{
var context = HttpContext.Current;
if (context == null)
return false;
var signal = ErrorSignal.FromContext(context);
if (signal == null)
return false;
signal.Raise(e, context);
return true;
}
private static bool RaiseEntityFrameWorkValidationErrorSignal(DbEntityValidationException e)
{
var context = HttpContext.Current;
if (context == null)
return false;
var signal = ErrorSignal.FromContext(context);
if (signal == null)
return false;
//Taken from post above
var errors = new List<string>();
foreach (var entityError in e.EntityValidationErrors)
{
errors.AddRange(entityError.ValidationErrors.Select(e2 => string.Join("Validation Error :: ", e2.PropertyName, " : ", e2.ErrorMessage)));
}
var error = string.Join("\r\n", errors);
var betterException = new Exception(error, e);
signal.Raise(betterException, context);
return true;
}
}
and then I register the attribute in the WebApiConfig.cs file under App_Start
config.Filters.Add(new ElmahHandleWebApiErrorAttribute());