I have a very simple controller action that takes in a viewmodel. I simply want to check the model in code and if it's not valid, dump the modelstate back as a BadRequest.
[HttpPost]
[Route("SaveBraceStep1")]
[SwaggerOperation(OperationId = "SaveBraceStep1")]
[ProducesResponseType(200, Type = typeof(VM.ProjectBraceDataModelStep1))]
public async Task<IActionResult> SaveBraceStep1(VM.ProjectBraceDataModelStep1 model)
{
if (!ModelState.IsValid)
{
return new BadRequestObjectResult(ModelState.Errors());
}
var project = await bracingDataService.SaveBraceStep1(model);
return Ok(project);
}
When the result comes back to Chrome, it's as expected.
I have an http interceptor as follows:
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, } from '#angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
export class HttpErrorInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request)
.pipe(catchError(err => {
// how can I read the results of the ModelState here
console.log(err);
return throwError('An error has been thrown');
}));
}
}
Here is what is dumped to the console
I've scoured thru tons of google searches and user blobs, but I can't seem to find an accepted concise way to do this.
I'm really blown away since I would expect this to be pretty much standardized by now.
Any Thoughts? Thank you in advance. Happy Coding!
~Mike
OK, so I figured this out with the help from https://www.strathweb.com/2018/07/centralized-exception-handling-and-request-validation-in-asp-net-core/
You basically have to register a new Action Type, then insert it into the pipeline.
public Task ExecuteResultAsync(ActionContext context)
{
var modelStateEntries = context.ModelState.Where(e => e.Value.Errors.Count > 0).ToArray();
var errors = new List<ValidationError>();
var details = "See ValidationErrors for details";
if (modelStateEntries.Any())
{
if (modelStateEntries.Length == 1 && modelStateEntries[0].Value.Errors.Count == 1 && modelStateEntries[0].Key == string.Empty)
{
details = modelStateEntries[0].Value.Errors[0].ErrorMessage;
}
else
{
foreach (var modelStateEntry in modelStateEntries)
{
foreach (var modelStateError in modelStateEntry.Value.Errors)
{
var error = new ValidationError
{
Name = modelStateEntry.Key,
Description = modelStateError.ErrorMessage
};
errors.Add(error);
}
}
}
}
var problemDetails = new ValidationProblemDetails
{
Status = 400,
Title = "Request Validation Error",
Instance = $"urn:myorganization:badrequest:{Guid.NewGuid()}",
Detail = details,
ValidationErrors = errors
};
context.HttpContext.Response.StatusCode = 400;
var json = JsonConvert.SerializeObject(problemDetails);
context.HttpContext.Response.WriteAsync(json);
return Task.CompletedTask;
}
}
public class ValidationProblemDetails : ProblemDetails
{
public ICollection<ValidationError> ValidationErrors { get; set; }
}
public class ValidationError
{
public string Name { get; set; }
public string Description { get; set; }
}
Then in startup.cs register it.
services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = ctx => new ValidationProblemDetailsResult();
});
Then wired up in the interceptor
#Injectable()
export class HttpErrorInterceptor implements HttpInterceptor {
constructor(private uiService: Services.UIService, private injector: Injector) {
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request)
.pipe(catchError((err: HttpErrorResponse) => {
const reader = new FileReader();
reader.addEventListener('loadend', (e) => {
const result = this.buildResponse(JSON.parse(e.srcElement['result']));
this.uiService.HttpError(result);
});
reader.readAsText(err.error);
return throwError(err);
}));
}
buildResponse(e: any): ApplicationHttpErrorResponse {
const model: ApplicationHttpErrorResponse = { ValidationErrors: [] };
if (e.ValidationErrors && e.ValidationErrors.length > 0) {
for (let i = 0; i < e.ValidationErrors.length; i++) {
const validator: ApplicationHttpError = {
Name: e.ValidationErrors[i].Name,
Description: e.ValidationErrors[i].Description
};
model.ValidationErrors.push(validator);
}
}
return model;
}
}
It can then be used in the UI service to display issues to the end user.
Related
I'm trying to create a POST endpoint that will allow me to upload a single File in an API. I'm using swagger.
The problem is: no matter what I do, the "upload button" is not shown in my Swagger page.
This is my controller API post call:
[HttpPost("{groupId:int:min(1)}/validate")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status502BadGateway)]
public async Task<IActionResult> UploadDocument(IFormFile file)
{
// TODO: handle file upload
return await Task.FromResult(Ok());
}
I have a FileUploadFilter that written like this:
public class FileUploadFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var formParameters = context.ApiDescription.ParameterDescriptions
.Where(paramDesc => paramDesc.IsFromForm());
if (formParameters.Any())
{
// already taken care by swashbuckle. no need to add explicitly.
return;
}
if (operation.RequestBody != null)
{
// NOT required for form type
return;
}
if (context.ApiDescription.HttpMethod == HttpMethod.Post.Method)
{
var uploadFileMediaType = new OpenApiMediaType() {
Schema = new OpenApiSchema() {
Type = "object",
Properties =
{
["files"] = new OpenApiSchema()
{
Type = "array",
Items = new OpenApiSchema()
{
Type = "string",
Format = "binary"
}
}
},
Required = new HashSet<string>() { "files" }
}
};
operation.RequestBody = new OpenApiRequestBody {
Content = { ["multipart/form-data"] = uploadFileMediaType }
};
}
}
}
public static class Helper
{
internal static bool IsFromForm(this ApiParameterDescription apiParameter)
{
var source = apiParameter.Source;
var elementType = apiParameter.ModelMetadata?.ElementType;
return (source == BindingSource.Form || source == BindingSource.FormFile)
|| (elementType != null && typeof(IFormFile).IsAssignableFrom(elementType));
}
}
and I'm injecting it into the Swagger Definition like this:
c.OperationFilter<FileUploadFilter>();
No matter what I do, the produces swagger page presents me with the following:
A "path" parameter named "groupId"
A request body that does not have an "upload button" and just says: file string($binary)
How can I display an "upload button" in my swagger home page? Any help would be greatly appreciated.
Nevermind. I was not "hitting" the "try it out" button. My bad. It works like a charm
So I have my endpoint defined like the following:
[ApiController]
[Route("load/stuff")]
public class SignUp : ControllerBase
{
IGoogleRecaptchaV3Service _gService { get; set; }
public SignUp(IGoogleRecaptchaV3Service gService)
{
_gService = gService;
}
[HttpPost]
public async Task<IActionResult> Post([FromQuery] SignUpModel SignUpData, GRequestModel.Factory grequestFactory)
{
GRequestModel grm = grequestFactory("resValue", "remipValue");
_gService.InitializeRequest(grm);
if (!await _gService.Execute())
{
//return error codes string.
return Ok(_gService.Response.error_codes);
}
//call Business layer
return base.Content("Content here", "text/html");
}
}
This should return the HTML content if the reCAPTCHA score is human-like.
Let me know how to debug this further and whether any further code is required.
UPDATE fetch JS Code
function loadStuff() {
if (location.pathname === "/test") {
grecaptcha.execute('recaptchasitekeyhere', { action: 'onloadafterdelay' }).then(function (token) {
console.log(token);
return fetch("/load/stuff?RecaptchaToken=" + token, {
method: "POST",
body: token,
})
}).then((response) => {
// console.log works here too
if (!response.ok) {
const errorBuild = {
type: "Error",
message: response.message || "Something went wrong",
data: response.data || "",
code: response.code || "",
};
console.log("Error: " + JSON.stringify(errorBuild));
}
response.text().then(body => {
//console.log(body);
document.getElementById("test1").innerHTML = body.split(' ')[0];
document.getElementById("test2").innerHTML = body.split(' ')[1];
});
}
)
}
}
I also added this in the program.cs file:
builder.Services.AddControllers();
// FIX TEST
builder.Services.AddTransient<GRequestModel.Factory>(serviceProvider =>
(string res, string remip) => new GRequestModel(serviceProvider.GetRequiredService<IConfiguration>(), res, remip));
//Register dependencies
builder.Services.AddRazorPages();
// REMOVE ME IN PRODUCTION, USE DI INSTEAD
// ....
Configuration = app.Configuration;
// ...
public partial class Program
{
internal static IConfiguration Configuration { get; private set; }
}
I added that code above as a temporary fix, but then I tried to implement dependency injection for the IConfiguration and my codebase got dirty. I'm still a beginner in C# and I'm learning by trial and error hence so many mistakes.
I need to sent custom exceptions message to client.
I have the following code:
in Startup.cs ConfigureServices method
services.AddGrpc(options => options.Interceptors.Add<ErrorInterceptor>());
in ErrorInterceptor.cs
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation)
{
try
{
return await continuation(request, context);
}
catch (ValidationException validationExc)
{
await WriteResponseHeadersAsync(StatusCode.InvalidArgument, translation =>
translation.GetEnumTranslation(validationExc.Error, validationExc.Parameters));
}
catch (Exception)
{
await WriteResponseHeadersAsync(StatusCode.Internal, translation =>
translation.GetEnumTranslation(HttpStatusCode.InternalServerError));
}
return default;
Task WriteResponseHeadersAsync(StatusCode statusCode, Func<ITranslationService, string> getMessage)
{
var httpContext = context.GetHttpContext();
var translationService = httpContext.RequestServices.GetService<ITranslationService>();
var errorMessage = getMessage(translationService);
var responseHeaders = new Metadata
{
{ nameof(errorMessage) , errorMessage },//1) can see in browser's devTools, but not in the code
{ "content-type" , errorMessage },//2) ugly, but works
};
context.Status = new Status(statusCode, errorMessage);//3) not working
return context.WriteResponseHeadersAsync(responseHeaders);//4) alternative?
}
}
in mask-http.service.ts
this.grpcClient.add(request, (error, reply: MaskInfoReply) => {
this.grpcBaseService.handleResponse<MaskInfoReply.AsObject>(error, reply, response => {
const mask = new Mask(response.id, response.name);
callback(mask);
});
});
in grpc-base.service.ts
handleResponse<T>(error: ServiceError,
reply: {
toObject(includeInstance?: boolean): T;
},
func: (response: T) => void) {
if (error) {
const errorMessage = error.metadata.headersMap['content-type'][0];
this.toasterService.openSnackBar(errorMessage, "Ok");
console.error(error);
return;
}
const response = reply.toObject();
func(response);
}
I wanted to send error using Status (comment 3), but it doesn't get changed
I wonder if there is an alternative way to send it not in response headers (comment 4)
I tried to add custom response header (comment 1), but the only one I received in client code was 'content-type' so I decided to overwrite it (comment 2)
I hit the same dead end recently and decided to do it this way:
Create an error model:
message ValidationErrorDto {
// A path leading to a field in the request body.
string field = 1;
// A description of why the request element is bad.
string description = 2;
}
message ErrorSynopsisDto {
string traceTag = 1;
repeated ValidationErrorDto validationErrors = 2;
}
Create an extension for the error model that serializes the object to JSON:
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
public static class ErrorSynopsisDtoExtension
{
public static string ToJson(this ErrorSynopsisDto errorSynopsisDto) =>
JsonConvert.SerializeObject(
errorSynopsisDto,
new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
}
Create a custom exception that encapsulates error model:
public class OperationException : Exception
{
private readonly List<ValidationErrorDto> validationErrors = new();
public bool HasValidationErrors => this.validationErrors.Count > 0;
public OperationException(string traceTag) : base
(
new ErrorSynopsisDto
{
TraceTag = traceTag
}.ToJson() // <- here goes that extension
) => ErrorTag = traceTag;
public OperationException(
string traceTag,
List<ValidationErrorDto> validationErrors
) : base
(
new ErrorSynopsisDto
{
TraceTag = traceTag,
ValidationErrors = { validationErrors }
}.ToJson() // <- here goes that extension again
)
{
ErrorTag = traceTag;
this.validationErrors = validationErrors;
}
}
Throw custom exception from service call handlers:
throw new OperationException(
"MY_CUSTOM_VALIDATION_ERROR_CODE",
// the following block can be simplified with a mapper, for reduced boilerplate
new()
{
new()
{
Field = "Profile.FirstName",
Description = "Is Required."
}
}
);
And lastly, the exception interceptor:
public class ExceptionInterceptor : Interceptor
{
private readonly ILogger<ExceptionInterceptor> logger;
public ExceptionInterceptor(ILogger<ExceptionInterceptor> logger) => this.logger = logger;
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation
)
{
try
{
return await continuation(request, context);
}
catch (OperationException ex)
{
this.logger.LogError(ex, context.Method);
var httpContext = context.GetHttpContext();
if (ex.HasValidationErrors)
{
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
}
else
{
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
}
throw;
}
catch (Exception ex)
{
this.logger.LogError(ex, context.Method);
var httpContext = context.GetHttpContext();
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
var opEx = new OperationException("MY_CUSTOM_INTERNAL_ERROR_CODE");
throw new RpcException(
new Status(
StatusCode.Internal,
opEx.Message
)
);
}
}
}
On the TypeScript-based frontend, I simply catch RPC errors and hydrate the message like this:
JSON.parse(err.message ?? {}) as ErrorSynopsisDto
Here is my code for Method in TypeScript
Ive been trying to pass an number[] array to the controller to send to a quickbooks api but all i really need right now is to get the values into the controller,
This is been done in Angular 5 .net core 2.0 latest version
The data hits the Post Method with no error and i have breakpoints everywhere it never reaches the controller.
import { Injectable } from '#angular/core';
import { Customer } from '../models/customer';
import { Vendor } from '../models/vendor';
import { Item } from '../models/item';
import { Invoice } from '../models/invoice';
import { CreditNote } from '../models/creditNote';
import { PPO } from '../models/ppo';
import { PO } from '../models/po';
import { AppSettings } from '../models/appSettings';
import { Http, Response, Headers, RequestOptions, RequestMethod, URLSearchParams } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import { MsgResult } from '../models/msgResult';
import { Router, ActivatedRoute } from '#angular/router';
import { isPlatformBrowser, isPlatformServer } from '#angular/common';
#Injectable()
export class SyncDataService {
errorMessage: string = "";
baseURL: string = 'http://localhost:56199/api';
constructor(private _http: Http, private route: ActivatedRoute, private router: Router) {}
syncCustomers(ids: Array<number>) {
var headers = new Headers();
headers.append('Content-Type', 'application/json; charset=utf-8');
var localStorage1 = localStorage.getItem('access_token');
if (localSt
orage1 != undefined) {
var token = JSON.parse(localStorage1);
//headers.append('Authorization', 'bearer ' + token);
//return this._http.post(this.baseURL + '/customer', ids, options)
// .map((response: Response) => <string>response.json())
// .catch(err => {
// return this.handleError(err);
// });
var stringids = JSON.stringify({ customerIDs: ids });
this._http.post(this.baseURL + '/customer/PostCust',
stringids).subscribe(result => result.json()), err => {
return this.handleError(err);
}
}
}
}
Here is my controller
[Route("api/[controller]")]
public class CustomerController : Controller
{
private readonly SyncDbContext _dbContext;
public CustomerController(SyncDbContext dbContext)
{
_dbContext = dbContext;
}
[HttpGet]
public List<Customer> Get()
{
return new SyncDataManager().GetCustomers();
}
[HttpPost("[action]")]
[AllowAnonymous]
public JsonResult PostCust([FromBody]int[] customerIDs)
{
// call quicbooks api and pass them the customers
// once quickbooks verifys the customer and sends us back a reference
// pass the quickbooks customer to SyncDataManager
var sync = new SyncDataManager();
var results = sync.UpdateCustomers(customerIDs);
var failedResults = results.Where(m => m.Success == false).ToList();
if(failedResults.Count == 0)
{
var json = new JsonResult("Updated Successfully");
json.StatusCode = 200;
return json;
}
else
{
var error = new StringBuilder();
foreach (var errorMessage in failedResults)
{
//string output = errorMessage.ErrorMessage.Substring(errorMessage.ErrorMessage.IndexOf('.') + 1);
string output = errorMessage.ErrorMessage;
error.AppendLine(output);
}
var json = new JsonResult(error.ToString());
json.StatusCode = 400;
return json;
}
}
There is no error messages and when i use break points on my controller, It does not hit the break points, been at it for 3 days no break through Please help
try in your Controller
[Route("api/[controller]/[action]")] //<--include action
public class CustomerController : Controller
{
...
[HttpGet,ActionName("Get")] //<--I don't know if it's necesary
public List<Customer> Get() {..}
[HttpPost, ActionName("PostCust")] //<--give there the "actionName
[AllowAnonymous]
public JsonResult PostCust([FromBody]int[] customerIDs){...}
}
this._http.post(this.baseURL + '/customer/PostCust',
this.customerIDs).subscribe((data:Response) => { this.resu = (data.json() as string) },
error => {
alert(error.json());
},
() => {
alert("Completed");
} else {
this.RefreshCustomers();
}
});
this.resu is my results variable
and no headers or casts for this.customerIDs this worked for me
I've a problem for sending data to the server via POST/DELETE.
I always get a CORS error when I'm trying to send data to the WebAPI.
The client-side was implemented with AngularJS and the server-side with ASP.NET WebAPI by C#.
Here's the Code of server-side WebAPI:
//Model:
public class TestRepository : ITestRepository
{
private List<Person> persons = new List<Person>();
public TestRepository()
{
Add(new Fond { id = "DE001", fname = "John", age = 32 });
Add(new Fond { id = "DE002", fname = "Jeffrey", age = 23 });
}
public IEnumerable<Person> GetAll()
{
return persons;
}
public Person Add(Person item)
{
if (item == null) {
throw new ArgumentNullException();
}
persons.Add(item);
return item;
}
public bool Delete(string id)
{
fonds.RemoveAll(p => p.id == id);
return true;
}
}
//Controller
public class TestController : ApiController
{
static readonly ITestRepository rep = new TestRepository();
// GET api/fond
public IEnumerable<Person> GetAllPersons()
{
return rep.GetAll();
}
// POST api/fond
[HttpPost]
public Person PostPerson(Person item)
{
return rep.Add(item);
}
// DELETE api/fond/5
public bool DeletePerson(string id)
{
if (rep.Delete(id))
{
return true;
}
else
{
return false;
}
}
I've installed the Nuget Package Microsoft.AspNet.WebApi.Cors.
In the WebApiConfig file I've added following lines:
...
var cors = new EnableCorsAttribute("http://localhost:63831", "*", "*");
config.EnableCors(cors);
And the client-side Code:
//Get the list works!
$scope.list = ResService.person.query();
//Delete doesn't work
$scope.deletePerson = function (removePers) {
$scope.res = ResService.person.remove(function () {
$log.info('[Resource]', $scope.res);
$scope.res.$delete(removeItem);
});
};
//Post doesn't work
$scope.addPerson = function (newPers) {
var persObj = {
id: newPers.id,
fname: newPers.fname,
age: newPers.age
};
$http.post(baseUrl + '/api/person', persObj).success(function (persData) {
$scope.list.push(persData);
}).error(function (message) {
$log.error(message);
});
Only GetAll function works. When I'm using POST or DELETE then comes an error message Cross-Origin-Request blocked..