Links
New projet
dotnet new webapi -o [project-name] --no-https
On VS to not open the web brower when you start the application: right-click on the project → Properties → Debug → uncheck Launch browser
Controllers/ItemsController.cs
[Produces ("application/json" )]
[ApiVersion ("1" )]
[ApiController ]
[Route ("[controller]" )]
public class ItemController : ControllerBase
{
private readonly IItemRepository itemRepository ;
private readonly IMapper mapper ;
public ItemsController (IItemRepository itemRepository , IMapper mapper )
{
this .itemRepository = itemRepository;
this .mapper = mapper;
GET
[HttpGet ("{id:int}" )]
[ProducesResponseType (typeof(ItemResponse), StatusCodes.Status200OK)]
[ProducesResponseType (StatusCodes.Status404NotFound)]
public async Task <IActionResult > GetByIdAsync (int id , CancellationToken cancellationToken )
{
var item = await context.Items
.AsNoTracking ()
.FirstOrDefaultAsync (x => x .Id == id , cancellationToken );
return item is null ? NotFound () : Ok (mapper .Map <ItemResponse >(item ));
}
[HttpGet ]
[ProducesResponseType (typeof(IReadOnlyCollection<ItemResponse>), StatusCodes.Status200OK)]
[ProducesResponseType (typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
public async Task <IActionResult > GetAsync ([FromQuery ] ItemQuery query , CancellationToken cancellationToken )
{
var predicate = PredicateBuilder.New <Item >(true );
if (!string .IsNullOrEmpty (query .Name ))
{
predicate = predicate.And (x => EF .Functions .Like (x .Name , $"%{query .Name } %" ));
}
if (query.Ids != null && query.Ids.Count > 0 )
{
predicate = predicate.And (x => query .Ids .Contains (x .Id ));
}
var items = await context.Items
.Where (predicate )
.AsNoTracking ()
.ToListAsync (cancellationToken );
return Ok (mapper .Map <IReadOnlyCollection <ItemResponse >>(items ));
}
POST
[HttpPost ]
[ProducesResponseType (typeof(ItemResponse), StatusCodes.Status201Created)]
[ProducesResponseType (typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
public async Task <IActionResult > CreateItemAsync (CreateUpdateItemQuery query , CancellationToken cancellationToken )
{
var item = this .mapper.Map <Item >(query );
await context.AddAsync (item , cancellationToken );
await context.SaveChangesAsync (cancellationToken );
var response = this .mapper.Map <ItemResponse >(item );
return Created (new Uri (HttpContext .Request .Path ), response);
return Created ($"https://localhost:5001/api/items/{createdItem .Id } " , response );
return CreatedAtAction (nameof (Get ), new { id = createdItem .Id }, response);
return CreatedAtRoute ("api" , new {
controller = "items" ,
action = nameof (Get ),
id = $"{response.Id }"
}, response);
}
[HttpPut ("{id}" )]
[ProducesResponseType (StatusCodes.Status204NoContent)]
[ProducesResponseType (typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType (StatusCodes.Status404NotFound)]
public async Task <IActionResult > UpdateAsync (int id , CreateUpdateItemQuery query )
{
var itemToUpdate = await context.Items.FirstOrDefaultAsync (x => x .Id == id , cancellationToken );
if (itemToUpdate is null )
{
return NotFound ();
}
this .mapper.Map (query , itemToUpdate );
await context.SaveChangesAsync (cancellationToken );
return NoContent ();
}
DELETE
[HttpDelete ("{id:int}" )]
[ProducesResponseType (StatusCodes.Status204NoContent)]
[ProducesResponseType (StatusCodes.Status400BadRequest)]
[ProducesResponseType (StatusCodes.Status404NotFound)]
public async Task <IActionResult > DeleteItemAsync (int id , CancellationToken cancellationToken )
{
if (id <= 0 )
{
return BadRequest ("id must be greater than 0." );
}
if (!await context.Transactions.AnyAsync (x => x .Id == id , cancellationToken ))
{
return NotFound (id );
}
context.Remove (new Item { Id = id });
await context.SaveChangesAsync (cancellationToken );
return NoContent ();
}
In case of exception the media type returned is application/problem+json which is not handled by default on the client side.
Program.cs
if (app.Environment.IsDevelopment ())
{
app.UseExceptionHandler ("/error-development" );
}
else
{
app.UseExceptionHandler ("/error" );
}
Controllers/ErrorController.cs
[ApiController ]
public class ErrorController : ControllerBase
{
[Route ("/error" )]
[ApiExplorerSettings (IgnoreApi = true )]
public IActionResult HandleError () => Problem ();
[Route ("/error-development" )]
[ApiExplorerSettings (IgnoreApi = true)]
public IActionResult HandleErrorDevelopment ([FromServices ] IHostEnvironment hostEnvironment )
{
if (!hostEnvironment.IsDevelopment ())
{
return NotFound ();
}
var exceptionHandlerFeature = HttpContext.Features.Get <IExceptionHandlerFeature >()!;
return Problem (
detail : exceptionHandlerFeature .Error .StackTrace ,
title : GetFinalInnerException (exceptionHandlerFeature .Error ).Message );
static Exception GetFinalInnerException (Exception exception )
=> exception.InnerException is null ? exception : GetFinalInnerException (exception .InnerException );
}
}
Handle application/problem+json on the client side
Clients/ItemClient.cs
private static IEnumerable<MediaTypeFormatter> problemJsonMediaTypeFormatters
=> new MediaTypeFormatter [1 ] { new ProblemJsonMediaTypeFormatter () };
var response = await httpClient.GetAsync (uri , cancellationToken );
if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
{
var validationProblemDetails =
await response.Content.ReadAsAsync <ValidationProblemDetails >(
problemJsonMediaTypeFormatters , cancellationToken );
MediaTypeFormatters/ProblemJsonMediaTypeFormatter.cs
public class ProblemJsonMediaTypeFormatter : JsonMediaTypeFormatter
{
private static readonly MediaTypeHeaderValue problemJsonMediaType = new ("application/problem+json" );
public ProblemJsonMediaTypeFormatter ()
{
SupportedMediaTypes.Add (problemJsonMediaType );
}
}
Fluent Validation
dotnet add package FluentValidation.AspNetCore
Program.cs
builder.Services.AddFluentValidationAutoValidation ();
builder.Services.AddValidatorsFromAssemblyContaining <Program >();
Validators/ItemValidator.cs
public class CreateItemQueryValidator : AbstractValidator <CreateItemQuery >
{
public CreateItemQueryValidator ()
{
RuleFor (x => x .Name ).NotEmpty ();
RuleFor (x => x .Description )
.MinimumLength (3 )
.Unless (x => string .IsNullOrEmpty (x .Description ));
Program.cs
builder.Services
.AddHealthChecks ()
.AddDbContextCheck <MyAppContext >();
app.MapHealthChecks ("/health" );
HTTP Code
Description
200
healthy
503
unhealthy
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 ItemClient (HttpClient httpClient )
{
this .httpClient = httpClient;
queryBuilder = new QueryBuilder ();
queryBuilder.Add ("api-version" , APIVERSION );
}
GET
ReadAsAsync
private static IEnumerable<MediaTypeFormatter> problemJsonMediaTypeFormatters
=> new MediaTypeFormatter [1 ] { new ProblemJsonMediaTypeFormatter () };
public async Task <OperationResult <IReadOnlyCollection <ItemResponse >>> GetItemsAsync (ItemQuery query , CancellationToken cancellationToken )
{
ArgumentNullException.ThrowIfNull (query );
var uri = new Uri ($"{PATH } {query .ToQueryString ()} " , UriKind .Relative );
var response = await httpClient.GetAsync (uri , cancellationToken );
if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
{
var validationProblemDetails =
await response.Content.ReadAsAsync <ValidationProblemDetails >(
problemJsonMediaTypeFormatters , cancellationToken );
return validationProblemDetails is null
? throw new InvalidDataException ($"{nameof (validationProblemDetails )} is null" )
: new OperationResult <IReadOnlyCollection <ItemResponse >>(validationProblemDetails.Errors );
}
response.EnsureSuccessStatusCode ();
var items = await response.Content.ReadAsAsync <IReadOnlyCollection <ItemResponse >>(cancellationToken );
return new OperationResult <IReadOnlyCollection <ItemResponse >>(items);
}
afficher Clients/OperationResult.cs
public class OperationResult <T >
{
public T? Result { get ; set ; }
public IDictionary <string , string []> ValidationErrors { get ; }
public IReadOnlyCollection<string > Errors => ValidationErrors.SelectMany (x => x .Value ).ToList ();
public bool IsValid => ValidationErrors.Count == 0 ;
public OperationResult (IDictionary <string , string []> validationErrors )
{
ValidationErrors = validationErrors;
}
public OperationResult (IEnumerable <ValidationFailure > validationFailures )
{
ValidationErrors = validationFailures
.GroupBy (x => x .PropertyName , x => x .ErrorMessage )
.ToDictionary (x => x .Key , x => x .ToArray ());
}
public OperationResult (T result )
{
Result = result;
ValidationErrors = new Dictionary <string , string []>();
}
}
afficher MediaTypeFormatters/ProblemJsonMediaTypeFormatter.cs
public class ProblemJsonMediaTypeFormatter : JsonMediaTypeFormatter
{
private static readonly MediaTypeHeaderValue problemJsonMediaType = new ("application/problem+json" );
public ProblemJsonMediaTypeFormatter ()
{
SupportedMediaTypes.Add (problemJsonMediaType );
}
}
afficher Extensions/QueryExtensions.cs
public static class QueryExtensions
{
public static QueryString ToQueryString (this ItemQuery query )
{
var queryBuilder = new QueryBuilder
{
{ nameof (query.PageIndex), query.PageIndex.ToString (CultureInfo .InvariantCulture ) },
{ nameof (query .PageSize ), query.PageSize .ToString (CultureInfo .InvariantCulture ) }
};
queryBuilder.AddIfValueNotNullNorEmpty (nameof (query .MyProperty ), query.MyProperty );
return queryBuilder.ToQueryString ();
}
}
afficher Extensions/QueryBuilderExtensions.cs
public static class QueryBuilderExtensions
{
public static void AddIfValueNotNullNorEmpty (this QueryBuilder source , string key , string value )
{
if (string .IsNullOrEmpty (value ))
{
return ;
}
source.Add (key , value );
}
}
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 } " );
}
}
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 ();
}
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é.