I've searched this intensive but can't get it to work.
I've haven an Web AP2 OData API and need to return a custom error class.
Here is what I have:
public class Error
{
public string Code { get; set; }
public string Message { get; set; }
}
In Global.asax.cs
protected void Application_Start()
{
GlobalConfiguration.Configure(WebApiConfig.Register);
GlobalConfiguration.Configuration.MessageHandlers.Add(new MessageInterceptor());
}
public class MessageInterceptor : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return base.SendAsync(request, cancellationToken).ContinueWith(
task =>
{
var body = task.Result.Content.ReadAsStringAsync().Result;
var resultObj = JsonConvert.DeserializeObject(body.Replace("value", "Results"));
task.Result.Content = new ObjectContent(typeof(object), resultObj, new JsonMediaTypeFormatter());
return task.Result;
}, cancellationToken);
}
}
In WebApiConfig,cs
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Filters.Add(new HandleApiExceptionAttribute());
}
}
public class HandleApiExceptionAttribute : ExceptionFilterAttribute, IExceptionFilter
{
public override void OnException(HttpActionExecutedContext context)
{
if (context.Exception is Error)
{
var res = context.Exception.Message;
//Define the Response Message
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.InternalServerError)
{
Content = new StringContent(res),
ReasonPhrase = res
};
//Create the Error Response
context.Response = response;
}
}
public class OrdersController : ODataControllerA controller
{
private Context db = new Context();
// GET: odata/Orders
[Queryable]
public IQueryable<Orders> GetOrders(ODataQueryOptions<Orders> opts)
{
<some code producing error>
Error theError = new Error()
{
Code = "1000",
Message = "Geen filter gespecificeerd"
};
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.BadRequest, theError);
//return Request.CreateResponse(HttpStatusCode.BadRequest, theError);
throw new HttpResponseException(response);
}
When I try this this crashes in the MessageInterceptor.
This is there because a third party consuming the API want's it in the specific format.
When the code runs correct it returns results{}
On error it should return Error{code:, message: }
Anyone ideas?
Jeroen
This is what I came up with:
I changed the controller to:
[Queryable, BasicAuthentication]
public IHttpActionResult GetOrders(ODataQueryOptions<Orders> opts)
{
else
{
Error theError = new Error()
{
Code = "1000",
Message = "Geen filter gespecificeerd"
};
return new ErrorResult(theError, Request);
}
return Ok(resultList);
}
Using this class:
public class ErrorResult : IHttpActionResult
{
Error _error;
HttpRequestMessage _request;
public ErrorResult(Error error, HttpRequestMessage request)
{
_error = error;
_request = request;
}
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
List<Error> _errorList = new List<Error>();
_errorList.Add(_error);
error err = new error()
{
errors = _errorList
};
var response = new HttpResponseMessage(HttpStatusCode.BadRequest)
{
Content = new ObjectContent<error>(err, new JsonMediaTypeFormatter()),
RequestMessage = _request
};
return Task.FromResult(response);
}
}
public class Error
{
public string Code { get; set; }
public string Message { get; set; }
}
public class error
{
public List<Error> errors { get; set; }
}
Related
In ASP.NET Core-6 Web API, I am implementing Fluent Validation. I have this code.
Model:
public class OAuthLoginRequest
{
public string username { get; set; }
public string password { get; set; }
}
public class OAuthLoginResponse
{
public string response_code { get; set; }
public string response_description { get; set; }
public Data data { get; set; }
public int size { get; set; }
public string access_token { get; set; }
public string refresh_token { get; set; }
public string expires_in { get; set; }
public string token_type { get; set; }
}
Validation:
public class OAuthLoginRequestValidator : AbstractValidator<OAuthLoginRequest>
{
public OAuthLoginRequestValidator()
{
RuleFor(user => user.username)
.NotNull()
.NotEmpty().WithMessage("Username field is required.");
RuleFor(user => user.password)
.NotNull()
.NotEmpty().WithMessage("Password field is required.");
}
}
AuthService:
public async Task<OAuthLoginResponse> Login(OAuthLoginRequest payload)
{
var response = new OAuthLoginResponse();
using (var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
try
{
var authEndpoint = _config.GetSection("Endpoints").GetValue<string>("authEndpoint");
string url = baseUrl + authEndpoint;
var request = new OAuthLoginRequest
{
username = payload.username,
password = payload.password
};
var header = new Dictionary<string, string> { };
var httpResponse = await _httpHelper.PostOrPutRequest(uri: url, methodType: HttpMethod.Post, model: request, headers: header);
if (httpResponse != null)
{
if (httpResponse.StatusCode == HttpStatusCode.OK)
{
var content = await httpResponse.Content.ReadAsStringAsync();
response = JsonConvert.DeserializeObject<OAuthLoginResponse>(content);
}
}
transaction.Complete();
}
catch (Exception ex)
{
_logger.Error("An Error occured " + ex.ToString());
response = null;
}
return response;
}
}
Controller:
[HttpPost]
[Route(ApiRoutes.Login)]
public async Task<ActionResult<OAuthLoginResponse>> Login([FromBody] OAuthLoginRequest request)
{
var result = await _myService.Login(request);
return Ok(result);
}
Dependency Injection:
public static class DIServiceExtension
{
public static void AddDependencyInjection(this IServiceCollection services)
{
// Validator
services.AddTransient<IValidator<OAuthLoginRequest>, OAuthLoginRequestValidator>();
}
}
Program.cs:
builder.Services.AddControllers()
.AddFluentValidation(options =>
{
// Automatic Validation
options.AutomaticValidationEnabled = false;
// Automatic registration of validators in assembly
options.RegisterValidatorsFromAssembly(Assembly.GetExecutingAssembly());
});
// Register Dependency Injection Service Extension
builder.Services.AddDependencyInjection();
var app = builder.Build();
I registered it in DIServiceExtension and then in Program.cs.
I deliberately post the Login without username and password, but the application did not display any validation message.
This is what I got in Postman:
Response body
{
"response_code": null,
"response_description": null,
"data": null,
"size": 0,
"access_token": null,
"refresh_token": null,
"expires_in": null,
"token_type": null
}
I expected it to display the validation message.
How can I resolve this?
From this GitHub comment,
options.AutomaticValidationEnabled = false;
is used to disable the automatic validation feature.
Approach 1: Automatic validation
Remove options.AutomaticValidationEnabled = false; from registering FluentValidation services.
Enable automatic validation with builder.Services.AddFluentValidationAutoValidation();.
using FluentValidation.AspNetCore;
builder.Services.AddControllers()
.AddFluentValidation(options =>
{
// Automatic registration of validators in assembly
options.RegisterValidatorsFromAssembly(Assembly.GetExecutingAssembly());
});
builder.Services.AddFluentValidationAutoValidation();
Reference: FluentValidation/FluentValidation.AspNetCore (Automatic Validation section)
Approach 2: Manual validation
In the Controller, get the injected IValidator<OAuthLoginRequest> service.
In the Login action, manually perform validation via await _validator.ValidateAsync(request);.
If fail validation, add the error(s) from the ValidationResult into ModelState and return the response with BadRequest.
public class AuthController : Controller
{
private readonly IValidator<OAuthLoginRequest> _validator;
public AuthController(IValidator<OAuthLoginRequest> validator)
{
_validator = validator;
}
[HttpPost]
[Route(ApiRoutes.Login)]
public async Task<ActionResult<OAuthLoginResponse>> Login([FromBody] OAuthLoginRequest request)
{
ValidationResult validationResult = await _validator.ValidateAsync(request);
if (!validationResult.IsValid)
{
// Add error into ModelState
validationResult.AddToModelState(ModelState);
return BadRequest(ModelState);
}
var result = await _myService.Login(request);
return Ok(result);
}
}
public static class FluentValidationExtensions
{
public static void AddToModelState(this ValidationResult result, ModelStateDictionary modelState)
{
foreach (var error in result.Errors)
{
modelState.AddModelError(error.PropertyName, error.ErrorMessage);
}
}
}
Reference: FluentValidation documentation (Manual Validation section)
I am trying to capture the exception when the route is not found and wrap the exception with the generic response model.
I tried to implement, as given in the answer to the question, but this solution also doesn't seem to work in my use case.
Because the status code 404 is also added to the response when the resource is not found, like when Id is not found.
app.UseStatusCodePages(new StatusCodePagesOptions()
{
HandleAsync = (ctx) =>
{
if (ctx.HttpContext.Response.StatusCode == 404)
{
throw new RouteNotFoundException("Route not found");
}
return Task.FromResult(0);
}
})
RouteNotFoundException
public class RouteNotFoundException : Exception
{
public RouteNotFoundException()
: base()
{
}
public RouteNotFoundException(string message)
: base(message)
{
}
}
ApiExceptionFilterAttribute
public class ApiExceptionFilterAttribute : ExceptionFilterAttribute
{
private readonly IDictionary<Type, Action<ExceptionContext>> _exceptionHandlers;
public ApiExceptionFilterAttribute()
{
// Register known exception types and handlers.
_exceptionHandlers = new Dictionary<Type, Action<ExceptionContext>>
{
{ typeof(RouteNotFoundException), HandleNotFoundException }
};
}
public override void OnException(ExceptionContext context)
{
HandleException(context);
base.OnException(context);
}
private void HandleException(ExceptionContext context)
{
Type type = context.Exception.GetType();
if (_exceptionHandlers.ContainsKey(type))
{
_exceptionHandlers[type].Invoke(context);
return;
}
HandleUnknownException(context);
}
private void HandleNotFoundException(ExceptionContext context)
{
var exception = context.Exception as RouteNotFoundException;
var details = new ProblemDetails()
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
Title = "The specified resource was not found.",
Detail = exception.Message
};
context.Result = new NotFoundObjectResult(details);
context.ExceptionHandled = true;
}
private void HandleUnknownException(ExceptionContext context)
{
var details = new ProblemDetails
{
Status = StatusCodes.Status500InternalServerError,
Title = "An error occurred while processing your request.",
Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1"
};
context.Result = new ObjectResult(details)
{
StatusCode = StatusCodes.Status500InternalServerError
};
context.ExceptionHandled = true;
}
}
ResponseWrapperMiddleware
public class ResponseWrapperMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ResponseWrapperMiddleware> _logger;
public ResponseWrapperMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
_logger = loggerFactory?.CreateLogger<ResponseWrapperMiddleware>() ?? throw new ArgumentNullException(nameof(loggerFactory));
}
public async Task Invoke(HttpContext httpContext)
{
try
{
var currentBody = httpContext.Response.Body;
using (var memoryStream = new MemoryStream())
{
//set the current response to the memorystream.
httpContext.Response.Body = memoryStream;
await _next(httpContext);
//reset the body
httpContext.Response.Body = currentBody;
memoryStream.Seek(0, SeekOrigin.Begin);
var readToEnd = new StreamReader(memoryStream).ReadToEnd();
var objResult = JsonConvert.DeserializeObject(readToEnd);
var result = CommonApiResponse.Create((HttpStatusCode)httpContext.Response.StatusCode, objResult, null);
await httpContext.Response.WriteAsync(JsonConvert.SerializeObject(result));
}
}
catch (Exception ex)
{
if (httpContext.Response.HasStarted)
{
_logger.LogWarning("The response has already started, the http status code middleware will not be executed.");
throw;
}
return;
}
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class ResponseWrapperMiddlewareExtensions
{
public static IApplicationBuilder UseResponseWrapperMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<ResponseWrapperMiddleware>();
}
}
Generic Response Model
public class CommonApiResponse
{
public static CommonApiResponse Create(HttpStatusCode statusCode, object result = null, string errorMessage = null)
{
return new CommonApiResponse(statusCode, result, errorMessage);
}
public string Version => "1.2.3";
public int StatusCode { get; set; }
public string RequestId { get; }
public string ErrorMessage { get; set; }
public object Result { get; set; }
protected CommonApiResponse(HttpStatusCode statusCode, object result = null, string errorMessage = null)
{
RequestId = Guid.NewGuid().ToString();
StatusCode = (int)statusCode;
Result = result;
ErrorMessage = errorMessage;
}
}
How to handle the error if the route is not found and capture the error in the generic model? What is the workaround for this case?
I trie to pass my item Reach_DeclarationBc
public class Reach_DeclarationBc
{
public int ArticleId { get; set; }
public DateTime DateDeclaration { get; set; }
}
I use this code for call my api
var client = new HttpClient();
client.BaseAddress = new Uri("http://localhost:3001/");
Reach_DeclarationBc reach_DeclarationBc = new Reach_DeclarationBc
{
ArticleId = 129,
DateDeclaration = DateTime.Now
};
Reach_DeclarationBc result = await client.PostJsonAsync<Reach_DeclarationBc>("http://localhost:3009/reach", reach_DeclarationBc);
But a this line this give me an error
Reach_DeclarationBc result = await client.PostJsonAsync<Reach_DeclarationBc>("http://localhost:3009/reach", reach_DeclarationBc);
The error is : "TypeLoadException: Could not load type 'Microsoft.JSInterop.Json' from assembly 'Microsoft.JSInterop, Version=3.1.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60'."
The using in my class is
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Projet_Airbus.Data;
using Projet_Airbus.Models;
using Projet_Airbus.Models.Declaration;
using Microsoft.AspNetCore.Blazor;
using System.Net.Http;
For solve i tri to use asp.net core 3.1 but this not work
This is how I usually do it.
I inject a singleton HttpClient and logger with a dependency injector like Ninject.
I make a generic SendAsync which can handle PUT/DELETE/POST/GET.
I make ApiResponse class that contains the properties I'm interested in.
I create a Request class (InitRequest) that I use for the request.
I create a Response class (InitResponse) that I use for the response.
I have TimeOutInMs to set the time out for the api call.
I have a logger that logs error.
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Mvc;
using Newtonsoft.Json;
public class HomeController : Controller
{
private readonly WebApi _webApi;
//Inject singleton httpclient and logger
public HomeController(HttpClient httpClient, ILogger logger)
{
_webApi = new WebApi(httpClient, logger);
_webApi.BaseAddress = "https://www.webapi.com/";
_webApi.TimeOutInMs = 2000;
}
public async Task<ActionResult> Index()
{
//You might want to move the web api call to a service class
var method = "init";
var request = new InitRequest
{
Id = 1,
Name = "Bob"
};
var response = await _webApi.SendAsync<InitResponse>(method, request, HttpMethod.Post);
if (response.StatusCode == HttpStatusCode.OK)
{
var viewModel = response.Data.ToIndexViewModel();
}
else
{
//Handle Error
}
return View(viewModel);
}
}
public class IndexViewModel
{
public string Name { get; set; }
public string Address { get; set; }
}
public static class ModelMapper
{
public static IndexViewModel ToIndexViewModel(this InitResponse response)
{
return new IndexViewModel
{
Name = response.Name,
Address = response.Address
};
}
}
public class InitRequest
{
public int Id { get; set; }
public string Name { get; set; }
}
public class InitResponse
{
public string Name { get; set; }
public string Address { get; set; }
}
public class WebApi
{
private readonly HttpClient _httpClient;
private readonly ILogger _logger;
public Uri BaseAddress { get; set; }
public int TimeOutInMs { get; set; }
public WebApi(HttpClient httpClient, ILogger logger)
{
_logger = logger ?? throw new Exception($"Missing constructor ceference - {nameof(logger)} can not be null!");
_httpClient = httpClient ?? throw new Exception($"Missing constructor ceference - {nameof(httpClient)} can not be null!");
}
public async Task<ApiResponse<TOut>> SendAsync<TOut>(string method, object param, HttpMethod httpMethod)
{
if (string.IsNullOrWhiteSpace(BaseAddress.ToString()))
throw new Exception($"{nameof(BaseAddress)} can not be null or empty.");
if (string.IsNullOrWhiteSpace(method))
throw new Exception($"{nameof(method)} can not be null or empty.");
var paramListForLog = JsonConvert.SerializeObject(param);
//Set timeout
if (TimeOutInMs <= 0)
{
TimeOutInMs = (int)TimeSpan.FromSeconds(100.0).TotalMilliseconds;
}
var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromMilliseconds(TimeOutInMs));
var cancellationToken = cts.Token;
var url = new Uri($"{BaseAddress}{method}", UriKind.Absolute);
try
{
HttpResponseMessage response;
using (var request = new HttpRequestMessage(httpMethod, url))
{
//Add content
if (param != null)
{
var content = JsonConvert.SerializeObject(param);
request.Content = new StringContent(content, Encoding.UTF8, "application/json");
}
//Add headers
request.Headers.Accept.Clear();
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
_logger.Info($"Calling {httpMethod} for {url}", paramListForLog);
//Send the request
response = await _httpClient.SendAsync(request, cancellationToken);
}
//If success
if (response.IsSuccessStatusCode)
{
_logger.Info($"Successfully called {httpMethod} for {url}", paramListForLog);
var data = await response.Content.ReadAsAsync<TOut>(cancellationToken);
return new ApiResponse<TOut>
{
StatusCode = response.StatusCode,
Data = data
};
}
//If failure
var error = await response.Content.ReadAsStringAsync();
_logger.Error($"An error occured calling {httpMethod} for {url}. Error was {error}", paramListForLog);
return new ApiResponse<TOut>
{
StatusCode = response.StatusCode,
Message = error
};
}
//If timeout
catch (OperationCanceledException ex)
{
var message = cancellationToken.IsCancellationRequested ?
$"Request timed out after {TimeOutInMs} ms occured calling {httpMethod} for {url}. Error was: {ex.Message}" :
$"An error occured calling {httpMethod} for {url}. Error was: {ex.Message}";
var webEx = new Exception(message, ex);
_logger.Error(webEx, webEx.Message, paramListForLog);
return new ApiResponse<TOut>
{
StatusCode = HttpStatusCode.RequestTimeout,
Message = message
};
}
//If unknown error
catch (Exception ex)
{
var webEx = new Exception($"An error occured calling {httpMethod} for {url}. Error was: {ex.Message}", ex);
_logger.Error(webEx, webEx.Message, paramListForLog);
throw webEx;
}
}
}
public interface ILogger
{
void Info(string message, string param);
void Error(string message, string param);
void Error(Exception e, string message, string param);
}
public class ApiResponse<T>
{
public HttpStatusCode StatusCode { get; set; }
public string Message { get; set; }
public T Data { get; set; }
}
I have 2 viewmodel, aviewmodel and bviewmodel. in aviewmodel and bviewmodel are used method GetListEmployees(). i have a problem.
In aviewmodel,I have successfully run the GetListEmployees().but in bviewmodel then it does not work. I debug and I see it was thrown when it came to response. I don't know why it's like that.
video description. https://youtu.be/CD3lHKx3igs
Function to get the list.
async private Task GetListEmployees()
{
var result = await GeneralMethods.Ins.GetDataFromDB<Employee>("Employee/employees");
// ListEmployee = new ObservableCollection<Employee>(result);
}
This is class read api.
public class GeneralMethods
{
private static GeneralMethods _Ins;
public static GeneralMethods Ins
{
get
{
if (_Ins == null)
_Ins = new GeneralMethods();
return GeneralMethods._Ins;
}
private set
{
GeneralMethods._Ins = value;
}
}
public async Task<List<T>> GetDataFromDB<T>(string urlDB)
{
string url = "http://webapi.local/api/" + urlDB;
using (HttpResponseMessage response = await APIHelper.ApiClient.GetAsync(url))
{
if (response.IsSuccessStatusCode)
{
var result = response.Content.ReadAsAsync<List<T>>().Result;
return result;
}
else
{
throw new Exception(response.ReasonPhrase);
}
}
}
}
APIHelper
public static class APIHelper
{
public static HttpClient ApiClient { get; set; }
public static void InitializeClient()
{
ApiClient = new HttpClient();
ApiClient.DefaultRequestHeaders.Accept.Clear();
ApiClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
}
I am not an expert in asp.net webapi and trying to return some custom errorresponse with a custom error model. Please find below my code. Is this the right approach? basically i have a custom errormodel which i need it to be returned in case of any exception in webapi.
public async Task<HttpResponseMessage> Get(string assetId)
{
_logProvider.Info($"AssetId - {assetId}");
try
{
return Request.CreateResponse(HttpStatusCode.OK, result);
}
catch (Exception exception)
{
return Request.CreateCustomResponse(HttpStatusCode.InternalServerError, exception.Message);
}
}
public static class ApiExtensions
{
public static HttpResponseMessage CreateCustomResponse(this HttpRequestMessage request, HttpStatusCode statusCode, string errorMessage)
{
var errorMessageModel = new ErrorModel(statusCode, errorMessage);
return request.CreateResponse(statusCode, errorMessageModel);
}
}
public class ErrorModel
{
public ErrorModel(HttpStatusCode statusCode, string message)
{
Code = (int)statusCode;
Message = message;
Fields = new Dictionary<string, ModelErrorCollection>();
}
public ErrorModel(HttpStatusCode statusCode)
{
Code = (int)statusCode;
Fields = new Dictionary<string, ModelErrorCollection>();
}
public string Message { get; set; }
public int Code { get; set; }
public Dictionary<string, ModelErrorCollection> Fields { get; set; }
}
Thanks