I have concerns on the way that we returns errors to client.
Do we return error immediately by throwing HttpResponseException when we get an error:
public void Post(Customer customer)
{
if (string.IsNullOrEmpty(customer.Name))
{
throw new HttpResponseException("Customer Name cannot be empty", HttpStatusCode.BadRequest)
}
if (customer.Accounts.Count == 0)
{
throw new HttpResponseException("Customer does not have any account", HttpStatusCode.BadRequest)
}
}
Or we accumulate all errors then send back to client:
public void Post(Customer customer)
{
List<string> errors = new List<string>();
if (string.IsNullOrEmpty(customer.Name))
{
errors.Add("Customer Name cannot be empty");
}
if (customer.Accounts.Count == 0)
{
errors.Add("Customer does not have any account");
}
var responseMessage = new HttpResponseMessage<List<string>>(errors, HttpStatusCode.BadRequest);
throw new HttpResponseException(responseMessage);
}
This is just a sample code, it does not matter either validation errors or server error, I just would like to know the best practice, the pros and cons of each approach.
This question is related to
c#
rest
asp.net-web-api
For those errors where modelstate.isvalid is false, I generally send the error as it is thrown by the code. Its easy to understand for the developer who is consuming my service. I generally send the result using below code.
if(!ModelState.IsValid) {
List<string> errorlist=new List<string>();
foreach (var value in ModelState.Values)
{
foreach(var error in value.Errors)
errorlist.Add( error.Exception.ToString());
//errorlist.Add(value.Errors);
}
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.BadRequest,errorlist);}
This sends the error to the client in below format which is basically a list of errors:
[
"Newtonsoft.Json.JsonReaderException: **Could not convert string to integer: abc. Path 'Country',** line 6, position 16.\r\n
at Newtonsoft.Json.JsonReader.ReadAsInt32Internal()\r\n
at Newtonsoft.Json.JsonTextReader.ReadAsInt32()\r\n
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ReadForType(JsonReader reader, JsonContract contract, Boolean hasConverter, Boolean inArray)\r\n
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)",
"Newtonsoft.Json.JsonReaderException: **Could not convert string to integer: ab. Path 'State'**, line 7, position 13.\r\n
at Newtonsoft.Json.JsonReader.ReadAsInt32Internal()\r\n
at Newtonsoft.Json.JsonTextReader.ReadAsInt32()\r\n
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ReadForType(JsonReader reader, JsonContract contract, Boolean hasConverter, Boolean inArray)\r\n
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)"
]
You can throw a HttpResponseException
HttpResponseMessage response =
this.Request.CreateErrorResponse(HttpStatusCode.BadRequest, "your message");
throw new HttpResponseException(response);
Building up upon Manish Jain
's answer (which is meant for Web API 2 which simplifies things):
1) Use validation structures to response as many validation errors as possible. These structures can also be used to response to requests coming from forms.
public class FieldError
{
public String FieldName { get; set; }
public String FieldMessage { get; set; }
}
// a result will be able to inform API client about some general error/information and details information (related to invalid parameter values etc.)
public class ValidationResult<T>
{
public bool IsError { get; set; }
/// <summary>
/// validation message. It is used as a success message if IsError is false, otherwise it is an error message
/// </summary>
public string Message { get; set; } = string.Empty;
public List<FieldError> FieldErrors { get; set; } = new List<FieldError>();
public T Payload { get; set; }
public void AddFieldError(string fieldName, string fieldMessage)
{
if (string.IsNullOrWhiteSpace(fieldName))
throw new ArgumentException("Empty field name");
if (string.IsNullOrWhiteSpace(fieldMessage))
throw new ArgumentException("Empty field message");
// appending error to existing one, if field already contains a message
var existingFieldError = FieldErrors.FirstOrDefault(e => e.FieldName.Equals(fieldName));
if (existingFieldError == null)
FieldErrors.Add(new FieldError {FieldName = fieldName, FieldMessage = fieldMessage});
else
existingFieldError.FieldMessage = $"{existingFieldError.FieldMessage}. {fieldMessage}";
IsError = true;
}
public void AddEmptyFieldError(string fieldName, string contextInfo = null)
{
AddFieldError(fieldName, $"No value provided for field. Context info: {contextInfo}");
}
}
public class ValidationResult : ValidationResult<object>
{
}
2) Service layer will return ValidationResult
s, regardless of operation being successful or not. E.g:
public ValidationResult DoSomeAction(RequestFilters filters)
{
var ret = new ValidationResult();
if (filters.SomeProp1 == null) ret.AddEmptyFieldError(nameof(filters.SomeProp1));
if (filters.SomeOtherProp2 == null) ret.AddFieldError(nameof(filters.SomeOtherProp2 ), $"Failed to parse {filters.SomeOtherProp2} into integer list");
if (filters.MinProp == null) ret.AddEmptyFieldError(nameof(filters.MinProp));
if (filters.MaxProp == null) ret.AddEmptyFieldError(nameof(filters.MaxProp));
// validation affecting multiple input parameters
if (filters.MinProp > filters.MaxProp)
{
ret.AddFieldError(nameof(filters.MinProp, "Min prop cannot be greater than max prop"));
ret.AddFieldError(nameof(filters.MaxProp, "Check"));
}
// also specify a global error message, if we have at least one error
if (ret.IsError)
{
ret.Message = "Failed to perform DoSomeAction";
return ret;
}
ret.Message = "Successfully performed DoSomeAction";
return ret;
}
3) API Controller will construct the response based on service function result
One option is to put virtually all parameters as optional and perform custom validation which return a more meaningful response. Also, I am taking care not to allow any exception to go beyond the service boundary.
[Route("DoSomeAction")]
[HttpPost]
public HttpResponseMessage DoSomeAction(int? someProp1 = null, string someOtherProp2 = null, int? minProp = null, int? maxProp = null)
{
try
{
var filters = new RequestFilters
{
SomeProp1 = someProp1 ,
SomeOtherProp2 = someOtherProp2.TrySplitIntegerList() ,
MinProp = minProp,
MaxProp = maxProp
};
var result = theService.DoSomeAction(filters);
return !result.IsError ? Request.CreateResponse(HttpStatusCode.OK, result) : Request.CreateResponse(HttpStatusCode.BadRequest, result);
}
catch (Exception exc)
{
Logger.Log(LogLevel.Error, exc, "Failed to DoSomeAction");
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, new HttpError("Failed to DoSomeAction - internal error"));
}
}
You can use custom ActionFilter in Web Api to validate model:
public class DRFValidationFilters : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (!actionContext.ModelState.IsValid)
{
actionContext.Response = actionContext.Request
.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
//BadRequest(actionContext.ModelState);
}
}
public override Task OnActionExecutingAsync(HttpActionContext actionContext,
CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() =>
{
if (!actionContext.ModelState.IsValid)
{
actionContext.Response = actionContext.Request
.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
}
});
}
public class AspirantModel
{
public int AspirantId { get; set; }
public string FirstName { get; set; }
public string MiddleName { get; set; }
public string LastName { get; set; }
public string AspirantType { get; set; }
[RegularExpression(@"^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$",
ErrorMessage = "Not a valid Phone number")]
public string MobileNumber { get; set; }
public int StateId { get; set; }
public int CityId { get; set; }
public int CenterId { get; set; }
[HttpPost]
[Route("AspirantCreate")]
[DRFValidationFilters]
public IHttpActionResult Create(AspirantModel aspirant)
{
if (aspirant != null)
{
}
else
{
return Conflict();
}
return Ok();
}
}
}
Register CustomAttribute class in webApiConfig.cs config.Filters.Add(new DRFValidationFilters());
For Web API 2 my methods consistently return IHttpActionResult so I use...
public IHttpActionResult Save(MyEntity entity)
{
....
return ResponseMessage(
Request.CreateResponse(
HttpStatusCode.BadRequest,
validationErrors));
}
If you are using ASP.NET Web API 2, the easiest way is to use the ApiController Short-Method. This will result in a BadRequestResult.
return BadRequest("message");
Use the built in "InternalServerError" method (available in ApiController):
return InternalServerError();
//or...
return InternalServerError(new YourException("your message"));
It looks like you're having more trouble with Validation than errors/exceptions so I'll say a bit about both.
Validation
Controller actions should generally take Input Models where the validation is declared directly on the model.
public class Customer
{
[Require]
public string Name { get; set; }
}
Then you can use an ActionFilter
that automatically sends validation messages back to the client.
public class ValidationActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
var modelState = actionContext.ModelState;
if (!modelState.IsValid) {
actionContext.Response = actionContext.Request
.CreateErrorResponse(HttpStatusCode.BadRequest, modelState);
}
}
}
For more information about this check out http://ben.onfabrik.com/posts/automatic-modelstate-validation-in-aspnet-mvc
Error handling
It's best to return a message back to the client that represents the exception that happened (with relevant status code).
Out of the box you have to use Request.CreateErrorResponse(HttpStatusCode, message)
if you want to specify a message. However, this ties the code to the Request
object, which you shouldn't need to do.
I usually create my own type of "safe" exception that I expect the client would know how to handle and wrap all others with a generic 500 error.
Using an action filter to handle the exceptions would look like this:
public class ApiExceptionFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
var exception = context.Exception as ApiException;
if (exception != null) {
context.Response = context.Request.CreateErrorResponse(exception.StatusCode, exception.Message);
}
}
}
Then you can register it globally.
GlobalConfiguration.Configuration.Filters.Add(new ApiExceptionFilterAttribute());
This is my custom exception type.
using System;
using System.Net;
namespace WebApi
{
public class ApiException : Exception
{
private readonly HttpStatusCode statusCode;
public ApiException (HttpStatusCode statusCode, string message, Exception ex)
: base(message, ex)
{
this.statusCode = statusCode;
}
public ApiException (HttpStatusCode statusCode, string message)
: base(message)
{
this.statusCode = statusCode;
}
public ApiException (HttpStatusCode statusCode)
{
this.statusCode = statusCode;
}
public HttpStatusCode StatusCode
{
get { return this.statusCode; }
}
}
}
An example exception that my API can throw.
public class NotAuthenticatedException : ApiException
{
public NotAuthenticatedException()
: base(HttpStatusCode.Forbidden)
{
}
}
Just to update on the current state of ASP.NET WebAPI. The interface is now called IActionResult
and implementation hasn't changed much:
[JsonObject(IsReference = true)]
public class DuplicateEntityException : IActionResult
{
public DuplicateEntityException(object duplicateEntity, object entityId)
{
this.EntityType = duplicateEntity.GetType().Name;
this.EntityId = entityId;
}
/// <summary>
/// Id of the duplicate (new) entity
/// </summary>
public object EntityId { get; set; }
/// <summary>
/// Type of the duplicate (new) entity
/// </summary>
public string EntityType { get; set; }
public Task ExecuteResultAsync(ActionContext context)
{
var message = new StringContent($"{this.EntityType ?? "Entity"} with id {this.EntityId ?? "(no id)"} already exist in the database");
var response = new HttpResponseMessage(HttpStatusCode.Ambiguous) { Content = message };
return Task.FromResult(response);
}
#endregion
}
ASP.NET Web API 2 really simplified it. For example, the following code:
public HttpResponseMessage GetProduct(int id)
{
Product item = repository.Get(id);
if (item == null)
{
var message = string.Format("Product with id = {0} not found", id);
HttpError err = new HttpError(message);
return Request.CreateResponse(HttpStatusCode.NotFound, err);
}
else
{
return Request.CreateResponse(HttpStatusCode.OK, item);
}
}
returns the following content to the browser when the item is not found:
HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8
Date: Thu, 09 Aug 2012 23:27:18 GMT
Content-Length: 51
{
"Message": "Product with id = 12 not found"
}
Suggestion: Don't throw HTTP Error 500 unless there is a catastrophic error (for example, WCF Fault Exception). Pick an appropriate HTTP status code that represents the state of your data. (See the apigee link below.)
Links:
Source: Stackoverflow.com