WebApplicationFactory doesn't (seem to) send out any data - asp.net-core

I am trying to create an integration test for my ASP.NET Core middleware using the WebApplicationFactory type.
My goal is to emulate an ASP.NET Core host, integrate app insights, and actually send data to a real Application Insights instance, to ensure everything is working as expected.
Unfortunately, my test server doesn't send any data to application insights.
This is what my test setup looks like. I have subclassed from WebApplicationFactory as follows:
public class WebTestFixture : WebApplicationFactory<Startup>
{
protected override IHostBuilder CreateHostBuilder()
{
var builder = Host.CreateDefaultBuilder();
builder.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.UseEnvironment("Testing");
});
builder.ConfigureAppConfiguration((context, config) =>
{
config.AddUserSecrets<WebTestFixture>();
});
return builder;
}
}
Then the test Startup class of my test assembly looks as follows:
public class Startup
{
public Startup(IConfiguration configuration) => Configuration = configuration;
public IConfiguration Configuration { get; set; }
public void ConfigureServices(IServiceCollection services)
{
services.AddApplicationInsightsTelemetry();
services.AddAppInsightsHttpBodyLogging();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseAppInsightsHttpBodyLogging();
app.UseEndpoints(endpoints =>
{
endpoints.MapPost("/", async ctx =>
{
ctx.Response.StatusCode = StatusCodes.Status400BadRequest;
await ctx.Response.WriteAsync("Hello from integration test");
});
});
}
}
The instrumentation key is stored in secrets.json.
{
"ApplicationInsights": {
"InstrumentationKey": "***HIDDEN***"
}
}
And finally my test
public class BodyLoggerMiddlewareIntegrationTests : IClassFixture<WebTestFixture>
{
private readonly WebTestFixture _factory;
public BodyLoggerMiddlewareIntegrationTests(WebTestFixture factory) => _factory = factory;
[Fact]
public async void BodyLoggerMiddleware_Should_Send_And_Mask_Data()
{
// Arrange
var client = _factory.CreateClient();
// Act
var requestBody = new
{
Name = "Bommelmaier",
Password = "Abracadabra!",
AccessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI"
};
var response = await client.PostAsync("/", new JsonContent(requestBody));
// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
}
}
The test passes and I can see the following line in the debug log (formatted for better readability), which tells me app insights send out my data.
Application Insights Telemetry:
{
"name": "AppRequests",
"time": "2021-12-22T19:13:32.8336126Z",
"iKey": "***HIDDEN***",
"tags": {
"ai.application.ver": "15.0.0.0",
"ai.cloud.roleInstance": "MY_HOST",
"ai.operation.id": "3fe600bbe13fcf41b1e52a0df7f7465e",
"ai.operation.name": "POST /",
"ai.internal.sdkVersion": "aspnet5c:2.18.0+a9fc6af7538cc287d263d9bc216c7910bfc34566",
"ai.internal.nodeName": "MY_HOST"
},
"data": {
"baseType": "RequestData",
"baseData": {
"ver": 2,
"id": "3857ad615d298e4b",
"name": "POST /",
"duration": "00:00:00.2006790",
"success": false,
"responseCode": "400",
"url": "http://localhost/",
"properties": {
"_MS.ProcessedByMetricExtractors": "(Name:'Requests', Ver:'1.1')",
"RequestBody": "{\"Name\":\"Bommelmaier\",\"Password\":\"Abracadabra!\",\"AccessToken\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI\"}",
"AspNetCoreEnvironment": "Production",
"ResponseBody": "Hello from integration test",
"DeveloperMode": "true"
}
}
}
}
However, nothing ever arrives at my App Insights instance, even after having waited more than 30min. Any idea? What am I missing here?

Thank you Andreas Wendl. Posting your suggestion as an answer to help other community members.
You can open Fiddler and have a look if there is outgoing traffic to the hosts mentioned here IP addresses used by Azure Monitor
Also make sure to allow these hosts/IPs in your outgoing network/firewall/proxy/etc. rules.

Related

How to make "One or more validation errors occurred" raise an exception?

I'm running a WebAPI on Core 3.1 and one of my endpoints excepts JSON with a model that has fields with [Required] attribute like so:
public class Vendor
{
public int ID { get; set; }
[Required(ErrorMessage = "UID is required")] <<<------ required attribute
public string UID { get; set; }
}
When i call this endpoint without setting UID I get the following output as expected:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "|7ced8b82-4aa34d65daa99a12.",
"errors": {
"Vendor.UID": [
"UID is required"
]
}
}
Altho this output is pretty informative and clear it is not consistent with other error outputs that my API produces by means of ExceptionFilter. Is there any way this errors can be routed to the exception filter as well?
Here is my Startup.cs:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
Common.Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
services.AddMvc().AddXmlDataContractSerializerFormatters();
services.AddMvc().AddMvcOptions(options =>
{
options.EnableEndpointRouting = false;
});
services.AddMvcCore(options => options.OutputFormatters.Add(new XmlSerializerOutputFormatter()));
services.AddOptions();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
{
//Middleware for exception filtering
app.UseMiddleware<ErrorHandlingMiddleware>(new ErrorHandlingMiddlewareOptions { logger = logger });
app.UseStaticFiles();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseMvc(routes =>
{
routes.MapRoute("EndpointNotFound", "{*url}", new { controller = "Error", action = "EndpointNotFound" });
});
}
}
You can add filter in your mvc service or controller service
this filter return badrequest
services.AddControllers(option =>
{
option.Filters.Add<ValidationFilter>();
});
to create your filter you can add this class
also you can customize this filter to what ever you want
public class ValidationFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
//before contrller
if(!context.ModelState.IsValid)
{
var errorsInModelState = context.ModelState
.Where(x => x.Value.Errors.Count > 0)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Errors.Select(x => x.ErrorMessage).ToArray());
var errorResponse = new ErrorResponse();
foreach (var error in errorsInModelState)
{
foreach (var subError in error.Value)
{
var errorModel = new ErrorModel
{
FieldName = error.Key,
Message = subError
};
errorResponse.Error.Add(errorModel);
}
context.Result = new BadRequestObjectResult(errorResponse);
return;
}
await next();
//after controller
}
}
}
I have created error model just like this
public class ErrorModel
{
public string FieldName { get; set; }
public string Message { get; set; }
}
and error response like below
public class ErrorResponse
{
public List<ErrorModel> Error { get; set; } = new List<ErrorModel>();
public bool Successful { get; set; }
}
In order to achive this functionality you need to implement your own model validator described in this question: Model validation in Web API - Exception is thrown with out a throw statement?
You might want to take a look at the great library FluentValidation!
Sample:
Build a validator module binding your DTO and create a set of rules.
public class CustomerValidator : AbstractValidator<Customer> {
public CustomerValidator() {
RuleFor(x => x.Surname).NotEmpty();
RuleFor(x => x.Forename).NotEmpty().WithMessage("Please specify a first name");
RuleFor(x => x.Discount).NotEqual(0).When(x => x.HasDiscount);
RuleFor(x => x.Address).Length(20, 250);
RuleFor(x => x.Postcode).Must(BeAValidPostcode).WithMessage("Please specify a valid postcode");
}
private bool BeAValidPostcode(string postcode) {
// custom postcode validating logic goes here
}
}
Inject it in your controllers through DI:
services.AddControllers()
.AddFluentValidation(s =>
{
s.ValidatorOptions.CascadeMode = CascadeMode.Stop;
s.RunDefaultMvcValidationAfterFluentValidationExecutes = false;
s.ValidatorOptions.LanguageManager.Culture = new CultureInfo("en-US");
s.RegisterValidatorsFromAssemblyContaining<Customer>();
...
// more validators
});
That way your code looks well organized;
You get rid of Data Annotations spread all over your code.
Personalize error messages and validations.
You also may want to check why implementing ControllerBase on your controllers might be the way to go while using web APIs.
You want to output the JSON like this:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "|7ced8b82-4aa34d65daa99a12.",
"errors": {
"Vendor.UID": [
"UID is required"
]
}
}
the type and the status field, are from here https://github.com/dotnet/aspnetcore/blob/v3.1.17/src/Mvc/Mvc.Core/src/DependencyInjection/ApiBehaviorOptionsSetup.cs#L54-L108, the ClientErrorMapping will be configured when the dotnet core project setup.
the JSON was a ValidationProblemDetails which created by the DefaultProblemDetailsFactory, https://github.com/dotnet/aspnetcore/blob/v3.1.17/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs#L45
we can use this ProblemDetailsFactory to create the ValidationProblemDetails https://github.com/dotnet/aspnetcore/blob/v3.1.17/src/Mvc/Mvc.Core/src/DependencyInjection/MvcCoreServiceCollectionExtensions.cs#L261
I have just change in http methos and work fine for the same issue
[HttpPost("authenticate")]
public IActionResult Authenticate([FromBody]AuthenticateModel model)

Is there anyone who can offer me a solution to this error? I can't understand where I'm wrong in configuring IdentityServer4

The image is here for Postman Request
It's a problem that I can't understand in the end, which is that it always gets a {"error": "invalid_request"}.
I request from Postman, using ImplicitFlow and OAuth 2.0, but I always get the same error.
I attach below the settings I made for IdentityServer4. I hope it will be useful to someone and can give me a solution.
public static class Config
{
public static IEnumerable<ApiResource> ApiResources()
{
return new[] {
new ApiResource("SkyEye.API", "SkyEye.API")
};
}
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId="ReactClient",
ClientName="SkyEye.SPA",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
RedirectUris = new List<string>
{
"https://localhost:5002",
},
PostLogoutRedirectUris = new []{
"https://localhost:5002"
},
AllowedScopes= {
// IdentityServerConstants.StandardScopes.Profile,
// IdentityServerConstants.StandardScopes.OpenId,
"SkyEye.API"
}
}
};
}
public static IEnumerable<TestUser> Users()
{
return new[]
{
new TestUser
{ SubjectId = "1",
Username = "lascodaniil",
Password="password"
}
};
}
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddTestUsers(Config.Users().ToList())
.AddInMemoryClients(Config.GetClients())
.AddInMemoryApiResources(Config.ApiResources());
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseDeveloperExceptionPage();
app.UseIdentityServer();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
}
I think your redirect URL is not valid. The URL in postman is different than the one provided in IDs configuration. The URL should be an exact match
Try changing this
RedirectUris = new List<string>
{
"https://localhost:5002",
},
to this
RedirectUris = new List<string>
{
"https://localhost:5002/connect/token",
},
Hope it helps

Processing unhandled exceptions in ASP.NET Core 3.1

In ASP.NET Core 3.1 a feature was added where unhandled exceptions can be passed onto an instance of ILogger as noted here:
Logging in .NET Core and ASP.NET Core
I have a Server side Blazor website where I want to be able to process these exceptions in a function where I can log them to a database or perhaps send an email. However I am unable to come up with code to do this based on the provided documentation. Could someone provide sample code to trap unhandled exceptions?
I recommand to use Serilog with sinks you need.
In your project add packages :
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Settings.Configuration
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.Debug
dotnet add package Serilog.Sinks.Seq
Serilog.AspNetCore to integrate Serilog with ASP.Net core.
Serilog.Settings.Configuration to read serilog config from .Net core configuration.
Serilog.Sinks.Console to write logs in the console.
Serilog.Sinks.Debug to write logs in Visual Studio output pane.
Serilog.Sinks.Seq to write logs in a Seq server, which is much more powerful than a DB.
Setup login in your Program.cs with :
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseSerilog((hostingContext, loggerConfiguration) => loggerConfiguration
.ReadFrom.Configuration(hostingContext.Configuration))
.Build();
And in Startup.cs:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Admin/Error");
}
app.UseSerilogRequestLogging()
And configure your logs in appsettings.json with:
"Serilog": {
"LevelSwitches": {
"$controlSwitch": "Information"
},
"MinimumLevel": {
"ControlledBy": "$controlSwitch"
},
"WriteTo": [
{
"Name": "Seq",
"Args": {
"serverUrl": "http://localhost:5341/",
"controlLevelSwitch": "$controlSwitch",
"apiKey": "{SeqApiKey}"
}
},
{
"Name": "Console"
},
{
"Name": "Debug"
}
],
"Enrich": [
"FromLogContext",
"WithMachineName",
"WithThreadId"
]
}
This tell Serilog to use the log level configure in Seq for your app.
"LevelSwitches": {
"$controlSwitch": "Information"
}
...
"Args": {
"serverUrl": "http://localhost:5341/",
"controlLevelSwitch": "$controlSwitch",
"apiKey": "{SeqApiKey}"
}
{SeqApiKey} is an Api key configure in your Seq server for your app.
If you want to use a DB, Serilog has long list of sinks you can use.
Doing some playing around, I found a way to do error logging without having to use a third party tool. Here is my code:
public class ExceptionLogger : ILogger
{
public IDisposable BeginScope<TState>(TState state)
{
return null;
}
public bool IsEnabled(LogLevel logLevel)
{
return logLevel == LogLevel.Error;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (logLevel == LogLevel.Error)
{
while (exception.InnerException != null)
exception = exception.InnerException;
LogException(exception);
}
}
private void LogException(Exception error)
{
...
}
public sealed class ExceptionLoggerProvider : ILoggerProvider
{
public ILogger CreateLogger(string categoryName)
{
return new ExceptionLogger();
}
public void Dispose()
{
}
}
And in Startup.cs add this:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddProvider(new ExceptionLoggerProvider());
...
If you want to do it by yourself, on top of your middleware pipe add:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Admin/Error");
}
app.Use(async (context, next) =>
{
try
{
await next().ConfigureAwait(false);
}
catch(Exception e)
{
LogException(e);
throw;
}
})

How to return indented json content from an OData controller in asp core web api?

I can retrieve intended json result from normal WebApi using following way.
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddJsonOptions(x=>
{
x.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented;
});
But I cannot find a way to output json like this when using ODataController as opposed to ControllerBase when web api is used. ODataController always sends a minified json.
public class EmployeeController : ODataController
{
[EnableQuery()]
public IActionResult Get()
{
return Ok(new BOContext().Employees.ToList());
}
}
Also, startup.cs
public class Startup
{
private static IEdmModel GetModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Employee>("Employee");
return builder.GetEdmModel();
}
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddOData();
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddJsonOptions(x=>
{
x.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.None;
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapODataServiceRoute("odata", "odata", GetModel());
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
The route is working and I am receiving correct data.
Is there a way to control and output indented json from an OData controller?
I suggest you to make the transfer with minified jsonm, but use json beutifier to show formatted json. dont do this on the data flow phase.
If you are using javascript on the front-end side. You can simple use
JSON.stringify(jsObj, null, "\t"); // stringify with tabs inserted at each level
JSON.stringify(jsObj, null, 2); // stringify with 2 spaces at each level
Not sure if this is still actual, but you can specify formatter when returning the data
// [...]
public IActionResult Get()
{
var res = Ok(_db.Employees);
res.Formatters.Add(new Microsoft.AspNetCore.Mvc.Formatters.JsonOutputFormatter(
new Newtonsoft.Json.JsonSerializerSettings() { Formatting = Newtonsoft.Json.Formatting.Indented },
System.Buffers.ArrayPool<char>.Create()));
return res;
}
And of course, if you want more generalized solution (or you just have a lot of code that is already written), you can create interim abstract class and inherit from that class instead of just ODataController:
public abstract class AbstractFormattedOdataController : ODataController
{
public override OkObjectResult Ok(object value)
{
var res = base.Ok(value);
res.Formatters.Add(new Microsoft.AspNetCore.Mvc.Formatters.JsonOutputFormatter(
new Newtonsoft.Json.JsonSerializerSettings() { Formatting = Newtonsoft.Json.Formatting.Indented },
System.Buffers.ArrayPool<char>.Create()));
return res;
}
}
// [...]
public class EmployeesController : AbstractFormattedOdataController
{
[EnableQuery()]
public IActionResult Get()
{
return Ok(new BOContext().Employees.ToList());
}
}

Is there a way to handle asp.net core odata errors

Is there a way to handle asp.net core odata errors?
I have a model class DimDateAvailable with one property, a primary key of int DateId, and I make a call like /data/DimDateAvailable?$select=test.
Other calls work as expected and return what I'm after - this is a deliberate call to generate a fault, and it fails because there is no property named test on the model. The response comes back as expected, like so: {"error":{"code":"","message":"The query specified in the URI is not valid. Could not find a property named 'test' on type 'DimDateAvailable'... followed by a stack trace.
This response is fine when env.IsDevelopment() is true but I don't want to expose the stack trace when not in development.
I've looked at wrapping the code in the controllers' get method in a try-catch, but I think there's an action filter running over the results so it never gets called. On the other hand, I can't see where to inject any middleware and/or add any filters to catch errors. I suspect there might be a way to override an output formatter to achieve what I want but I can't see how.
Here's what I have at the moment:
In Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<TelemetryDbContext>();
services.AddOData();
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc(routeBuilder =>
{
routeBuilder.MapODataServiceRoute("odata", "data", GetEdmModel());
routeBuilder.Select().Expand().Filter().OrderBy().MaxTop(null).Count();
// insert special bits for e.g. custom MLE here
routeBuilder.EnableDependencyInjection();
});
}
private static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<DimDateAvailable>("DimDateAvailable");
return builder.GetEdmModel();
}
In TelemetryDbContext.cs:
public virtual DbSet<DimDateAvailable> DimDateAvailable { get; set; }
In DimDateAvailable.cs
public class DimDateAvailable
{
[Key]
public int DateId { get; set; }
}
My controller:
public class DimDateAvailableController : ODataController
{
private readonly TelemetryDbContext data;
public DimDateAvailableController(TelemetryDbContext data)
{
this.data = data;
}
[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Supported, PageSize = 2000)]
public IActionResult Get()
{
return Ok(this.data.DimDateAvailable.AsQueryable());
}
}
This is in an asp.net core 2 web app with the Microsoft.AspNetCoreOData v7.0.1 and EntityFramework 6.2.0 packages.
Investigating Ihar's suggestion lead me down the rabbit hole, and I ended up inserting an ODataOutputFormatter into the MVC options to intercept ODataPayloadKind.Error responses and reformat them.
It was interesting to see that context.Features held an instance of IExceptionHandlerFeature in app.UseExceptionHandler() but not in the ODataOutputFormatter. That lack was pretty much what prompted me to pose this question in the first place, but was solved by translating the context.Object in the ODataOutputFormatter which is something I saw done in the OData source as well. I don't know if the changes below are good practice in asp.net core or when using the AspNetCoreOData package, but they do what I want for now.
Changes to Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<TelemetryDbContext>();
services.AddOData();
services.AddMvc(options =>
{
options.OutputFormatters.Insert(0, new CustomODataOutputFormatter(this.Environment.IsDevelopment()));
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Added this to catch errors in my own code and return them to the client as ODataErrors
app.UseExceptionHandler(appBuilder =>
{
appBuilder.Use(async (context, next) =>
{
var error = context.Features[typeof(IExceptionHandlerFeature)] as IExceptionHandlerFeature;
if (error?.Error != null)
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.ContentType = "application/json";
var response = error.Error.CreateODataError(!env.IsDevelopment());
await context.Response.WriteAsync(JsonConvert.SerializeObject(response));
}
// when no error, do next.
else await next();
});
});
app.UseMvc(routeBuilder =>
{
routeBuilder.MapODataServiceRoute("odata", "data", GetEdmModel());
routeBuilder.Select().Expand().Filter().OrderBy().MaxTop(null).Count();
// insert special bits for e.g. custom MLE here
routeBuilder.EnableDependencyInjection();
});
}
New classes CustomODataOutputFormatter.cs and CommonExtensions.cs
public class CustomODataOutputFormatter : ODataOutputFormatter
{
private readonly JsonSerializer serializer;
private readonly bool isDevelopment;
public CustomODataOutputFormatter(bool isDevelopment)
: base(new[] { ODataPayloadKind.Error })
{
this.serializer = new JsonSerializer { ContractResolver = new CamelCasePropertyNamesContractResolver() };
this.isDevelopment = isDevelopment;
this.SupportedMediaTypes.Add("application/json");
this.SupportedEncodings.Add(new UTF8Encoding());
}
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
{
if (!(context.Object is SerializableError serializableError))
{
return base.WriteResponseBodyAsync(context, selectedEncoding);
}
var error = serializableError.CreateODataError(this.isDevelopment);
using (var writer = new StreamWriter(context.HttpContext.Response.Body))
{
this.serializer.Serialize(writer, error);
return writer.FlushAsync();
}
}
}
public static class CommonExtensions
{
public const string DefaultODataErrorMessage = "A server error occurred.";
public static ODataError CreateODataError(this SerializableError serializableError, bool isDevelopment)
{
// ReSharper disable once InvokeAsExtensionMethod
var convertedError = SerializableErrorExtensions.CreateODataError(serializableError);
var error = new ODataError();
if (isDevelopment)
{
error = convertedError;
}
else
{
// Sanitise the exposed data when in release mode.
// We do not want to give the public access to stack traces, etc!
error.Message = DefaultODataErrorMessage;
error.Details = new[] { new ODataErrorDetail { Message = convertedError.Message } };
}
return error;
}
public static ODataError CreateODataError(this Exception ex, bool isDevelopment)
{
var error = new ODataError();
if (isDevelopment)
{
error.Message = ex.Message;
error.InnerError = new ODataInnerError(ex);
}
else
{
error.Message = DefaultODataErrorMessage;
error.Details = new[] { new ODataErrorDetail { Message = ex.Message } };
}
return error;
}
}
Changes to the controller:
[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Supported, PageSize = 2000)]
public IQueryable<DimDateAvailable> Get()
{
return this.data.DimDateAvailable.AsQueryable();
}
If you want a customization of responses, including customization of error responses try to use ODataQueryOptions instead of using
[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Supported, PageSize = 2000)]
Check some samples at https://learn.microsoft.com/en-us/aspnet/web-api/overview/odata-support-in-aspnet-web-api/supporting-odata-query-options#invoking-query-options-directly
It would allow you to cache validation errors and build custom response.
I have had this issue in the past and the only one way I got this working without having to write a middleware was like:
Try this:
catch (ODataException ex)
{
HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;//This line is important, if not it will return 500 Internal Server Error.
return BadRequest(ex.Message);//Just respond back the actual error which is 100% correct.
}
Then the error will look like:
{
"#odata.context": "http://yourendpoint.com$metadata#Edm.String",
"value": "The property 'test' cannot be used in the $select query option."
}
Hope this helps.
Thanks