« Asp.net core web api » : différence entre les versions
(→DELETE) |
|||
Ligne 253 : | Ligne 253 : | ||
return BadRequest(ModelState); | return BadRequest(ModelState); | ||
} | } | ||
</kode> | |||
== [https://docs.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-3.1#automatic-http-400-responses Automatic HTTP 400 responses] == | |||
L'attribut {{boxx|[ApiController]}} effectue automatiquement une validation du modèle et envoie une réponse HTTP 400 en cas d'erreur.<br> | |||
Il n'est donc plus nécessaire de tester {{boxx|ModelState.IsValid}}.<br> | |||
ASP.NET Core MVC utilise le filtre d'action {{boxx|ModelStateInvalidFilter}} pour tester la validité du modèle.<br> | |||
Exemple de réponse en cas d'erreur de validation du modèle: | |||
<kode lang='json'> | |||
{ | |||
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", | |||
"title": "One or more validation errors occurred.", | |||
"status": 400, | |||
"traceId": "|b659be6b-4fc34f5e9f3caf17.", | |||
"errors": { | |||
"Name": [ | |||
"The Name field is required." | |||
] | |||
} | |||
} | |||
</kode> | |||
<kode lang='cs'> | |||
// déserialiser la réponse | |||
var validationProblem = await JsonSerializer.DeserializeAsync<ValidationProblemDetails>( | |||
await response.Content.ReadAsStreamAsync()); | |||
</kode> | </kode> | ||
Version du 25 janvier 2020 à 23:05
Liens
- Create web APIs with ASP.NET Core
- Build web APIs with ASP.NET CoreBuild web APIs with ASP.NET Core
- Controller action return types in ASP.NET Core Web API
- StatusCodes
- Path
Projet
Clique-droit sur le projet → Properties → Debug → décocher Launch browser
Controller
Controllers/ItemsController.cs |
[Produces("application/json")] // force le format JSON [Route("api/[controller]")] // /api/items [ApiController] public class ItemsController : ControllerBase { private readonly IMyAppRepository _repository; public ListApiController(IMyAppRepository repository) { _repository = repository; } |
GET
// [HttpGet("[action]")] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<Item>))] public IActionResult Get() => Ok(_repository.GetAllItems()); [HttpGet("{id:int}")] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Item))] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] public IActionResult Get(int id) { try { var item = _repository.Get(id); if (item == null) return NotFound(); // status 404 Not Found return Ok(item); // status 200 OK } catch (Exception ex) { // using Microsoft.AspNetCore.Http; return StatusCode(StatusCodes.Status500InternalServerError, $"Failed to get Item {id} ({ex})"); } } |
POST
[HttpPost] [ProducesResponseType(StatusCodes.Status201Created, Type = typeof(Item))] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] public IActionResult Post([FromBody] Item item) { if (item == null) return BadRequest(); if (!ModelState.IsValid) return BadRequest(ModelState); try { var createdItem = _itemRepository.Add(item); return Created("item", createdItem); if (_repository.SaveAll()) { // status 201 Created return Created($"https://localhost:5001/api/items/{item.Id}", item); return CreatedAtAction(nameof(Get), new { id = item.Id }, item); return CreatedAtRoute("api", new { controller = "items", action = nameof(Get), id = $"{item.Id}" }, item); } else { return StatusCode(StatusCodes.Status500InternalServerError, "Failed to save."); } } catch (Exception ex) { return StatusCode(StatusCodes.Status500InternalServerError, $"Failed to add Item ({ex})"); } } |
Startup.cs |
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseMvc(routes => { routes.MapRoute( name: "api", template: "api/{controller=Items}/{id?}"); }); |
PUT / Update
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] public IActionResult Put([FromBody] Item item) { if (item == null) return BadRequest(); if (!ModelState.IsValid) return BadRequest(ModelState); var itemToUpdate = _itemRepository.Get(item.Id); if (itemToUpdate == null) return NotFound(); // status 404 Not Found _itemRepository.Update(item); return NoContent(); } |
DELETE
[HttpDelete("{id:int}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] public IActionResult Delete(int id) { if (id <= 0) return BadRequest(); var itemToDelete = _itemRepository.Get(id); if (itemToDelete == null) return NotFound(); _itemRepository.Remove(id); return NoContent(); } |
Client
Services/ItemDataService.cs |
public class ItemDataService : IItemDataService { private readonly HttpClient _httpClient; // injection de HttpClient public ItemDataService(HttpClient httpClient) { _httpClient = httpClient; } |
GET
public async Task<IEnumerable<Employee>> GetAllItemsAsync() { return await JsonSerializer.DeserializeAsync<IEnumerable<Item>>( await _httpClient.GetStreamAsync($"api/item"), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); } public async Task<Item> GetItemAsync(int itemId) { return await JsonSerializer.DeserializeAsync<Item>( await _httpClient.GetStreamAsync($"api/item/{itemId}"), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); } |
POST
public async Task<Item> AddItemAsync(Item item) { var itemJson = new StringContent(JsonSerializer.Serialize(item), Encoding.UTF8, "application/json"); var response = await _httpClient.PostAsync("api/item", itemJson); if (response.IsSuccessStatusCode) { return await JsonSerializer.DeserializeAsync<Item>(await response.Content.ReadAsStreamAsync()); } return null; } |
PUT
public async Task UpdateItemAsync(Item item) { var itemJson = new StringContent(JsonSerializer.Serialize(item), Encoding.UTF8, "application/json"); await _httpClient.PutAsync("api/item", itemJson); } |
DELETE
public async Task DeleteItemAsync(int itemId) { await _httpClient.DeleteAsync($"api/item/{itemId}"); } |
Validation
ItemViewModel.cs |
public class ItemViewModel { public int Id { get; set; } [Required] [MinLength(6)] public string Name { get; set; } } |
[HttpPost] public IActionResult Post([FromBody]ItemViewModel itemVm) { if (ModelState.IsValid) { var item = itemVm.ToEfItem(); _repository.Add(item); if (_repository.SaveAll()) { var newItemVm = new ItemViewModel(item); return Created($"/api/ListApi/{newItemVm.Id}", newItemVm); } else { return BadRequest("Failed to save."); } } else { return BadRequest(ModelState); } |
Automatic HTTP 400 responses
L'attribut [ApiController] effectue automatiquement une validation du modèle et envoie une réponse HTTP 400 en cas d'erreur.
Il n'est donc plus nécessaire de tester ModelState.IsValid.
ASP.NET Core MVC utilise le filtre d'action ModelStateInvalidFilter pour tester la validité du modèle.
Exemple de réponse en cas d'erreur de validation du modèle:
{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "traceId": "|b659be6b-4fc34f5e9f3caf17.", "errors": { "Name": [ "The Name field is required." ] } } |
// déserialiser la réponse var validationProblem = await JsonSerializer.DeserializeAsync<ValidationProblemDetails>( await response.Content.ReadAsStreamAsync()); |
Query Strings
Url: http://localhost:80/api/items?option1=true
ItemsController.cs |
[HttpGet] public IActionResult Get(bool option1 = false) // par défaut à false { } |
Images
Download
MyController.cs |
[HttpGet] public IActionResult Get() { var filePath = Path.Combine("~", "folder", "image.jpeg"); // VirtualFileResult return File(filePath, "image/jpeg"); } |
Base64
MyController.cs |
[HttpGet] public IActionResult Get() { var filePath = Path.Combine("~", "folder", "image.jpeg"); byte[] bytesArray = System.IO.File.ReadAllBytes(filePath); return Ok("data:image/jpeg;base64," + Convert.ToBase64String(bytesArray)); } |
Upload
MyController.cs |
[HttpPost] public async Task<IActionResult> Post([FromForm]IFormFile myFile) { var filePath = Path.Combine(folderPath, photo.FileName); using (var stream = new FileStream(filePath, FileMode.Create)) { await myFile.CopyToAsync(stream); } return Ok(filePath); } |
Postman → Body → form-data
- key: myFile
- File
- Value: Choose Files
Si myFile est toujours null, enlever [FromForm] |
Renvoyer le message d'erreur des exceptions
ExceptionFilterAttribute
ApiExceptionFilterAttribute.cs |
public class ApiExceptionFilterAttribute : ExceptionFilterAttribute { public override void OnException(ExceptionContext context) { context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; context.HttpContext.Response.ContentType = "text/plain"; context.HttpContext.Response.WriteAsync(context.Exception.Message).ConfigureAwait(false); } } |
MyController.cs |
[ApiExceptionFilter] [Route("api/[controller]")] public class MyController : Controller |
ExceptionMiddleware
ExceptionMiddleware.cs |
public class ExceptionMiddleware { public async Task Invoke(HttpContext context) { context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; var ex = context.Features.Get<IExceptionHandlerFeature>()?.Error; if (ex == null) return; context.Response.ContentType = "text/plain"; await context.Response.WriteAsync(ex.Message); } } |
Startup.cs |
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseExceptionHandler(new ExceptionHandlerOptions { ExceptionHandler = new ExceptionMiddleware().Invoke }); app.UseMvc(); |
Différencier la gestion d'erreur pour mvc et webapi
Utiliser un ApiExceptionFilterAttribute et un MvcExceptionFilter
UseWhen et UseExceptionHandler
Startup.cs |
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseWhen(x => x.Request.Path.Value.StartsWith("/api"), builder => { builder.UseExceptionHandler(new ExceptionHandlerOptions { ExceptionHandler = new ExceptionMiddleware().Invoke }); }); app.UseWhen(x => !x.Request.Path.Value.StartsWith("/api"), builder => { builder.UseExceptionHandler("/Home/Error"); }); |
UseExceptionHandler
ExceptionMiddleware.cs |
public class ExceptionMiddleware { public async Task Invoke(HttpContext context) { if (context.Request.Path.Value.StartsWith("/api")) { context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; var ex = context.Features.Get<IExceptionHandlerFeature>()?.Error; if (ex == null) return; context.Response.ContentType = "text/plain"; await context.Response.WriteAsync(ex.Message).ConfigureAwait(false); } else { // redirect to /Home/Error } } } |
UseStatusCodePagesWithReExecute
Startup.cs |
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseStatusCodePagesWithReExecute("/error/{0}"); app.UseExceptionHandler("/error/500"); app.UseMvc(); |
[HttpGet("/error/{code:int}")] public string Error(int code) { return $"Error: {code}"; } |
Erreurs
Unsupported Media Type with Postman
- Body → raw = json à envoyer
- Headers → Content-Type = application/json
JsonSerializationException: Self referencing loop detected for property 'xxx' ...
Startup.cs |
services.AddMvc() // ignorer les références circulaires entre objets dans EF pour JSON .AddJsonOptions(opt => opt.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore); |
SqlException: Cannot insert explicit value for identity column in table 'xxx' when IDENTITY_INSERT is set to OFF
Impossible de sauvegarder les changements si une entité a été insérée avec une valeur ≠ default dans une propriété bindée avec une colonne Identity.
Solution: forcer la valeur de cette propriété a default. La bdd mettra la valeur de la propriété a jour après avoir inséré l'entité.