Liens
New projet
dotnet new webapi -o [project-name] --no-https
Clique-droit sur le projet → Properties → Debug → décocher Launch browser
Controllers/ItemsController.cs
[ApiController ]
[Produces ("application/json" )]
[ApiVersion ("1" )]
[Route ("api/[controller]" )]
public class ItemsController : ControllerBase
{
private readonly IItemsRepository itemsRepository ;
private readonly IMapper mapper ;
public ItemsController (IItemsRepository itemsRepository , IMapper mapper )
{
this .itemsRepository = itemsRepository;
this .mapper = mapper;
GET
[HttpGet ]
[ProducesResponseType (typeof(IReadOnlyCollection<ItemDto>), StatusCodes.Status200OK)]
public async Task <IActionResult > GetAsync ([FromQuery ] ItemQuery query )
{
var predicate = PredicateBuilder.New <Item >(true );
if (query.Ids != null && query.Ids.Count > 0 )
{
predicate = predicate.And (x => query .Ids .Contains (x .Id ));
}
if (query.Name != null )
{
predicate = predicate.And (x => EF .Functions .Like (x .Name , $"%{query .Name } %" ));
}
var items = await context.Items.Where (predicate )
.ToListAsync ();
var itemsDto = mapper.Map <IReadOnlyCollection <ItemDto >>(items );
return Ok (items );
}
[HttpGet ("{id:int}" )]
[ProducesResponseType (typeof(ItemDto), StatusCodes.Status200OK)]
[ProducesResponseType (StatusCodes.Status400BadRequest)]
[ProducesResponseType (StatusCodes.Status404NotFound)]
[ProducesResponseType (StatusCodes.Status500InternalServerError)]
public async Task <IActionResult > GetByIdAsync (int id )
{
try
{
if (id <= 0 )
{
return BadRequest ();
}
var item = await context.FindAsync (id )
if (item == null )
{
return NotFound (nameof (id ));
}
var itemDto = mapper.Map <ItemDto >(item );
return Ok (itemDto );
}
catch (Exception ex )
{
return StatusCode (StatusCodes .Status500InternalServerError , $"Failed to get Item {id } ({ex } )" );
}
}
[HttpGet ("test" )]
public async Task <IActionResult > TestAsync () { }
POST
[HttpPost ]
[ProducesResponseType (typeof(ItemDto), StatusCodes.Status201Created)]
public async Task <IActionResult > CreateAsync (ItemDto itemDto )
{
var item = this .mapper.Map <Item >(itemDto );
var createdItem = await itemsRepository.AddAsync (item );
var createdItemDto = this .mapper.Map <ItemDto >(createdItem );
return Created (HttpContext .Request .Path , createdItemDto );
return Created ($"https://localhost:5001/api/items/{createdItem .Id } " , createdItemDto );
return CreatedAtAction (nameof (Get ), new { id = createdItem .Id }, createdItemDto );
return CreatedAtRoute ("api" , new {
controller = "items" ,
action = nameof (Get ),
id = $"{createdItem .Id }"
}, createdItemDto );
}
Startup.cs
public void Configure (IApplicationBuilder app , IHostingEnvironment env )
{
app.UseMvc (routes => {
routes .MapRoute (
name : "api" ,
template : "api/{controller=Items}/{id?}" );
});
[HttpPut ("{id}" )]
[ProducesResponseType (StatusCodes.Status204NoContent)]
[ProducesResponseType (StatusCodes.Status400BadRequest)]
[ProducesResponseType (StatusCodes.Status404NotFound)]
public async Task <IActionResult > UpdateAsync (int id , ItemDto itemDto )
{
if (id <= 0 )
{
return BadRequest ();
}
var itemToUpdate = await itemsRepository.GetByIdAsync (id );
if (itemToUpdate == null )
{
return NotFound ();
}
var item = this .mapper.Map <Item >(itemDto );
await itemsRepository.UpdateAsync (itemToUpdate , item );
return NoContent ();
}
DELETE
[HttpDelete ("{id:int}" )]
[ProducesResponseType (StatusCodes.Status204NoContent)]
[ProducesResponseType (StatusCodes.Status400BadRequest)]
[ProducesResponseType (StatusCodes.Status404NotFound)]
public async Task <IActionResult > DeleteAsync (int id )
{
if (id <= 0 )
{
return BadRequest ();
return BadRequest ("id must be greater than 0." );
return Problem (
title : "Bad Request" ,
detail : "id must be greater than 0." ,
statusCode : StatusCodes .Status400BadRequest );
}
var itemToDelete = await itemsRepository.GetByIdAsync (id );
if (itemToDelete == null )
return NotFound ();
await itemsRepository.DeleteAsync (id );
return NoContent ();
}
Client
Client/ItemClient.cs
public class ItemClient : IItemClient
{
private const string Path = "item" ;
private const string ApiVersion = "1" ;
private readonly HttpClient httpClient ;
private readonly QueryBuilder queryBuilder ;
public ItemDataService (HttpClient httpClient )
{
this .httpClient = httpClient;
queryBuilder = new QueryBuilder ();
queryBuilder.Add ("api-version" , ApiVersion );
}
GET
ReadAsAsync
public async Task <IReadOnlyList <ItemDto >> GetAllItemsAsync (CancellationToken cancellationToken )
{
var uri = new Uri ($"{Path } {queryBuilder .ToQueryString ()} " , UriKind .Relative );
var response = await this .httpClient.GetAsync (uri , cancellationToken );
response.EnsureSuccessStatusCode ();
var items = await response.Content.ReadAsAsync <IReadOnlyList <ItemDto >>(cancellationToken );
return items;
}
dotnet add package Microsoft.AspNet.WebApi.Client
JsonSerializer.DeserializeAsync
afficher public async Task <IReadOnlyList <ItemDto >> GetAllItemsAsync ()
{
return await JsonSerializer.DeserializeAsync <IEnumerable <Item >>(
await _ httpClient .GetStreamAsync ("api/item" ),
new JsonSerializerOptions ()
{
PropertyNameCaseInsensitive = true
});
var response = await _ httpClient.GetAsync ("api/item" );
if (!response.IsSuccessStatusCode)
throw new HttpRequestException (response.ReasonPhrase );
return await JsonSerializer.DeserializeAsync <IEnumerable <Item >>(
await response .Content .ReadAsStreamAsync (),
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
});
var response = await _ httpClient.GetAsync ($"item/{itemId } " );
if (!response.IsSuccessStatusCode)
throw new HttpRequestException (response.ReasonPhrase );
return await JsonSerializer.DeserializeAsync <Item >(
await response .Content .ReadAsStreamAsync ());
}
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" );
var response = await _ httpClient.PutAsync ("api/item" , itemJson );
if (!response.IsSuccessStatusCode)
{
var validationProblem = await JsonSerializer.DeserializeAsync <ValidationProblemDetails >(
await response .Content .ReadAsStreamAsync ());
}
}
DELETE
public async Task DeleteItemAsync (int itemId )
{
var response = await _ httpClient.DeleteAsync ($"api/item/{itemId } " );
if (!response.IsSuccessStatusCode)
{
var validationProblem = await JsonSerializer.DeserializeAsync <ValidationProblemDetails >(
await response .Content .ReadAsStreamAsync ());
throw new HttpRequestException ($"{validationProblem .Title } : {validationProblem .Detail } " );
}
}
Need nuget package Microsoft.AspNetCore.Mvc.Versioning
Controllers/ItemsController.cs
[ApiVersion ("1" )]
[ApiController ]
public class ItemsController : ControllerBase
Startup.cs
public void ConfigureServices (IServiceCollection services )
{
services.AddControllers ();
services.AddApiVersioning ();
Url: /api/items?api-version=1
Client/ItemClient.cs
this .httpClient.DefaultRequestHeaders.Add ("ip" , userIpAddress );
var response = await this .httpClient.PostAsync (uri , itemDtoJson );
Controllers/ItemController.cs
[HttpPost ]
public async Task <IActionResult > AddAsync (
[FromBody ] ItemDto itemDto ,
[FromHeader ] string ip )
{
var ip = this .HttpContext.Request.Headers["ip" ].SingleOrDefault ();
}
Startup.cs
public void ConfigureServices (IServiceCollection services )
{
services.AddHealthChecks ();
public void Configure (IApplicationBuilder app )
{
app.UseEndpoints (endpoints =>
{
endpoints .MapHealthChecks ("/health" );
});
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)
{
return BadRequest (ModelState );
}
var item = itemVm.ToEfItem ();
_ repository.Add (item );
if (_ repository.SaveAll ())
{
var new ItemVm = new ItemViewModel (item );
return Created ($"/api/ListApi/{newItemVm .Id } " , newItemVm );
}
else
{
return BadRequest ("Failed to save." );
}
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."
]
}
}
var content = await response.Content.ReadAsStreamAsync ();
var validationProblem = await JsonSerializer.DeserializeAsync <ValidationProblemDetails >(content );
dotnet add package FluentValidation.AspNetCore
Startup.cs
public void ConfigureServices (IServiceCollection services )
{
services.AddControllers ()
.AddFluentValidation ();
services.AddTransient <IValidator <ItemDto >, ItemValidator >();
Validation/ItemValidator.cs
public class ItemValidator : AbstractValidator <ItemDto >
{
public ItemValidator ()
{
RuleFor (x => x .Name ).NotEmpty ();
Returns manually generated validation problems
Controllers/ItemController.cs
public async Task <IActionResult > UpdateItemAsync (
int id ,
CreateUpdateItemQuery query ,
CancellationToken cancellationToken )
{
if (id <= 0 )
{
var details = new ValidationProblemDetails
{
Title = "One or more validation errors occurred." ,
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1"
};
details.Errors.Add (nameof (id ), new string [] { $"'id' must be greater than 0. You set the id to {id}." });
return ValidationProblem (details );
return Problem (
detail : "id must be greater than 0." , // error message
statusCode : StatusCodes .Status400BadRequest , // 500 by default
title : "One or more validation errors occurred." );
return BadRequest ("id must be greater than 0." );
Query Strings
Url: http://localhost:80/api/items?ids=1,2&retrieveDeleted=true
ItemClient.cs
var ids = new [] { 1 , 2 };
var retrieveDeleted = true ;
var queryBuilder = new QueryBuilder ();
queryBuilder.Add (nameof (ids ), string .Join ("," , ids ));
queryBuilder.Add (nameof (retrieveDeleted ), retrieveDeleted .ToString ());
var uri = new Uri ($"{Path } {queryBuilder .ToQueryString ()} " , UriKind .Relative );
var response = await this .httpClient.GetAsync (uri );
ItemController.cs
[HttpGet ]
public IActionResult Get ([FromQuery ] IReadOnlyCollection <int > ids , [FromQuery ] bool retrieveDeleted = false )
{ }
ItemQuery.cs
public class ItemQuery
{
public string Name { get ; set ; }
public int PageIndex { get ; set ; }
public int PageSize { get ; set ; }
}
ItemClient.cs
public async Task <IReadOnlyCollection <ItemDto >> GetAsync ()
{
var query = new ItemQuery {
ProfileName = string .Empty,
PageIndex = 0 ,
PageSize = 10
};
var uri = new Uri ($"{Path } {query .ToQueryString ()} " , UriKind .Relative );
ItemQueryExtension.cs
public static QueryString ToQueryString (this ItemQuery itemQuery )
{
var queryBuilder = new QueryBuilder ();
queryBuilder.Add (nameof (itemQuery .ProfileName ), itemQuery .Name );
queryBuilder.Add (nameof (itemQuery .PageIndex ), itemQuery .PageIndex .ToString ());
queryBuilder.Add (nameof (itemQuery .PageSize ), itemQuery .PageSize .ToString ());
return queryBuilder.ToQueryString ();
}
Program.cs
var builder = WebApplication.CreateBuilder (args );
builder.Services.AddTransient <IMyService , MyService >();
Images
Download
MyController.cs
[HttpGet ]
public IActionResult Get ()
{
var filePath = Path.Combine ("~" , "folder" , "image.jpeg" );
return File (filePath , "image/jpeg" );
}
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 ));
}
MyController.cs
[HttpPost ]
public async Task <IActionResult > Post ([FromForm ]IFormFile formFile )
{
var filePath = Path.Combine ("/path/to/folder" , formFile .FileName );
using (var stream = new FileStream (filePath, FileMode .Create ))
{
await formFile.CopyToAsync (stream );
}
return Ok (filePath );
}
Postman → Body → form-data
key: formFile
File
Value: Choose Files
Si formFile est toujours null , enlever [FromForm]
Renvoyer le message d'erreur des exceptions
Startup.cs
public void Configure (IApplicationBuilder app , IWebHostEnvironment env )
{
if (env.IsDevelopment ())
{
app.UseDeveloperExceptionPage ();
app.UseExceptionHandler ("/error-development" );
}
else
{
app.UseExceptionHandler ("/error" );
}
Controllers/ErrorController.cs
[ApiExplorerSettings (IgnoreApi = true)]
[ApiController ]
public class ErrorController : ControllerBase
{
[Route ("/error" )]
// default
// "title": "An error occured while processing your request."
// "status": 500, "traceId ": "00-...-00"
public IActionResult Error () => Problem ();
public IActionResult Error ()
{
var exceptionHandlerFeature = HttpContext.Features.Get <IExceptionHandlerFeature >()!;
return Problem (detail : $"Exception type: {exceptionHandlerFeature .Error .GetType ().Name } " );
}
[Route ("/error-development" )]
public IActionResult ErrorLocalDevelopment ([FromServices ] IWebHostEnvironment webHostEnvironment )
{
if (!hostEnvironment.IsDevelopment ())
{
return NotFound ();
}
var exceptionHandlerFeature = HttpContext.Features.Get <IExceptionHandlerFeature >()!;
return Problem (
detail : exceptionHandlerFeature .Error .StackTrace ,
title : exceptionHandlerFeature .Error .Message );
}
}
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.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 ();
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
{
}
}
}
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 } " ;
}
OperationResult.cs
public sealed class OperationResult <TResult , TError >
{
public TResult Result { get ; }
public TError Error { get ; }
public bool Success => Error == null ;
public OperationResult (TResult result )
{
Result = result;
}
public OperationResult (TError error )
{
Error = error;
}
}
OperationResult <MyDto , MyError > result ;
if ()
{
result = new OperationResult <MyDto , MyError >(myError);
}
else
{
result = new OperationResult <MyDto , MyError >(myDto);
}
return result;
By default .NET Core 3.0 uses System.Text.Json .
Use Newtonsoft.Json
dotnet add package Microsoft.AspNetCore.Mvc.NewtonsoftJson
Startup.cs
public void ConfigureServices (IServiceCollection services )
{
services.AddControllers ();
.AddNewtonsoftJson ();
Startup.cs
public void ConfigureServices (IServiceCollection services )
{
services.AddControllers (options =>
{
options .ModelBinderProviders .Insert (0 , new CommaSeparatedModelBinderProvider ());
});
CommaSeparatedModelBinderProvider.cs
public class CommaSeparatedModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder (ModelBinderProviderContext context )
{
if (context == null )
{
throw new ArgumentNullException (nameof (context));
}
if (context.Metadata.ModelType.GetInterface (nameof (IEnumerable )) != null
&& context.Metadata .ModelType != typeof (string ))
{
return new BinderTypeModelBinder (typeof (CommaSeparatedModelBinder ));
}
return null ;
}
}
afficher CommaSeparatedModelBinder.cs
public class CommaSeparatedModelBinder : IModelBinder
{
private static readonly MethodInfo ToArrayMethod = typeof (Enumerable).GetMethod ("ToArray" );
public Task BindModelAsync (ModelBindingContext bindingContext )
{
if (bindingContext == null )
{
throw new ArgumentNullException (nameof (bindingContext));
}
var modelType = bindingContext.ModelType;
var modelName = bindingContext.ModelName;
var valueType = modelType.GetElementType () ?? modelType .GetGenericArguments ().FirstOrDefault ();
if (modelType.GetInterface (nameof (IEnumerable )) != null
&& value Type != null
&& value Type .GetInterface (nameof (IConvertible )) != null )
{
var value ProviderResult = bindingContext .ValueProvider .GetValue (modelName );
if (valueProviderResult == ValueProviderResult.None)
{
return Task.CompletedTask;
}
bindingContext.ModelState.SetModelValue (modelName , valueProviderResult );
var value = valueProviderResult.FirstValue;
if (string .IsNullOrEmpty (value ))
{
return Task .CompletedTask ;
}
var list = (IList)Activator.CreateInstance (typeof (List <>).MakeGenericType (valueType ));
foreach (var splitValue in value .Split (new [] { ',' }))
{
if (!string .IsNullOrWhiteSpace (splitValue ))
{
list.Add (Convert .ChangeType (splitValue , valueType ));
}
}
var model = modelType.IsArray
? ToArrayMethod.MakeGenericMethod (valueType ).Invoke (this , new [] { list })
: list;
bindingContext.Result = ModelBindingResult.Success (model );
}
return Task.CompletedTask;
}
}
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é.