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
[ApiController ]
[Route ("[controller]" )]
[Produces ("application/json" )]
public class ItemController (IItemService itemService) : ControllerBase
{ }
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 > CreateAsync (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 );
var uri = new Uri ($"https://www.site.net/items/{createdItem .Id } " );
return Created (uri , response );
var actionName = nameof (ItemController.GetByIdAsync);
var controllerName = "Item" ;
var routeValues = new { id = item.Id };
return CreatedAtAction (actionName , controllerName , routeValues , response );
var routeValues = new
{
controller = "Item" ,
action = nameof (ItemController.GetByIdAsync),
id = item.Id
};
return CreatedAtRoute (routeValues , createdResource );
}
To handle Async suffix in CreatedAtAction
Program.cs
builder.Services.AddControllers (options =>
{
options .SuppressAsyncSuffixInActionNames = false ;
});
[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.Status404NotFound)]
public async Task <IActionResult > DeleteAsync (int id , CancellationToken cancellationToken )
{
if (!await context.Items.AnyAsync (x => x .Id == id , cancellationToken ))
{
return NotFound ();
}
context.Remove (new Item { Id = id });
await context.SaveChangesAsync (cancellationToken );
return NoContent ();
}
Response.Headers.Append ("Key" , "Value" );
Program.cs
builder.Services
.AddApiVersioning (options =>
{
options .ReportApiVersions = true ;
options .AssumeDefaultVersionWhenUnspecified = true ;
options .DefaultApiVersion = new ApiVersion (1 , 0 );
});
ItemController.cs
[ApiVersion ("1.0" )]
[Route ("v{version:apiVersion}/[controller]" )]
[ApiController ]
public class ItemController : ControllerBase
{ }
By default exceptions are returned in plain text. With the problem details service it returns now a problem details response.
In case of exception the media type returned is application/problem+json which is not handled by default on the client side.
Program.cs
builder.Services.AddProblemDetails ();
app.UseExceptionHandler ();
app.UseStatusCodePages ();
if (app.Environment.IsDevelopment ())
{
app.UseDeveloperExceptionPage ();
}
afficher Program.cs
if (app.Environment.IsDevelopment ())
{
app.UseExceptionHandler ("/error-development" );
}
else
{
app.UseExceptionHandler ("/error" );
}
afficher 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 );
}
}
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
CORS
Programs.cs
app.UseCors (x => x .SetIsOriginAllowed (origin => true ));
app.UseCors (x => x .WithOrigins ("https://localhost:4200" ));
Program.cs
builder.Services
.AddControllers (options =>
{
options .Conventions .Add (new RouteTokenTransformerConvention (new KebabCaseParameterTransformer ()));
})
KebabCaseParameterTransformer.cs
public partial class KebabCaseParameterTransformer : IOutboundParameterTransformer
{
[GeneratedRegex ("([a-z])([A-Z])" )]
private static partial Regex kebabCaseRegex ();
public string ? TransformOutbound (object ? value )
=> value == null ? null : kebabCaseRegex ().Replace (value .ToString (), "$1-$2").ToLower ();
}
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 );
}
afficher Clients/OperationResult.cs
public class OperationResult <T >
{
private readonly IDictionary <string , string []> validationErrors ;
public T? Result { get ; }
public IReadOnlyCollection<string > ErrorMessages => validationErrors.SelectMany (x => x .Value ).ToList ();
public bool IsValid => validationErrors.Count == 0 ;
public OperationResult (IDictionary <string , string []> validationErrors )
{
this .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 (string errorMessage )
{
validationErrors = new Dictionary <string , string []> { { string .Empty, new string [] { errorMessage } } };
}
public OperationResult (T result )
{
Result = result;
validationErrors = new Dictionary <string , string []>();
}
}
afficher Clients/OperationStatus.cs
public class OperationStatus
{
public string ErrorMessage { get ; }
public IDictionary <string , string []> Errors { get ; }
public bool Success => string .IsNullOrEmpty (ErrorMessage ) && Errors .Count == 0;
public OperationStatus ()
{
ErrorMessage = string .Empty;
Errors = new Dictionary <string , string []>();
}
public OperationStatus (string errorMessage )
{
ErrorMessage = errorMessage;
Errors = new Dictionary <string , string []>();
}
public OperationStatus (IDictionary <string , string []> errors )
{
ErrorMessage = string .Empty;
Errors = errors;
}
}
afficher Extensions/QueryExtensions.cs
public static class QueryExtensions
{
public static QueryString ToQueryString (this FetchItemQuery 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 );
}
}
Dependency Injection
Typed clients are transient objects usually injected into services.
They are expected to be short-lived.
If a typed client instance is captured in a singleton , it may prevent it from reacting to DNS changes, defeating one of the purposes of IHttpClientFactory.
Program.cs
services
.AddHttpClient <IMyClient , MyClient >(x =>
{
x .DefaultRequestHeaders .Authorization = // AuthenticationHeaderValue see below
x .BaseAddress = new Uri ("https://www.address.net" );
x.DefaultRequestHeaders.UserAgent.ParseAdd ("my-app" );
x.Timeout = TimeSpan.FromSeconds (100 );
})
.ConfigurePrimaryHttpMessageHandler (() => new HttpClientHandler
{
Proxy = new WebProxy
{
Address = new Uri ($"http://localhost:9000" ),
BypassProxyOnLocal = false ,
UseDefaultCredentials = false ,
},
});
Basic Base64 user password authentication
afficher var authenticationString = $"{username } :{password } " ;
var base64EncodedAuthenticationString = Convert.ToBase64String (Encoding .ASCII .GetBytes (authenticationString ));
x.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue ("Basic" , base64EncodedAuthenticationString);
AAD OAuth2 JWT authentication
afficher var builder = ConfidentialClientApplicationBuilder
.Create (clientId )
.WithClientSecret (clientSecret )
.WithAuthority (new Uri (authorityUri ))
.Build ();
var result = await builder
.AcquireTokenForClient ([scope ])
.ExecuteAsync ();
x.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue ("Bearer" , result.AccessToken );
Cognito OAuth2 JWT authentication
afficher var request = new HttpRequestMessage (HttpMethod .Post , tokenEndPoint);
var authHeader = Convert.ToBase64String (Encoding .UTF8 .GetBytes ($"{clientId } :{clientSecret } " ));
request.Headers.Authorization = new AuthenticationHeaderValue ("Basic" , authHeader);
request.Content = new FormUrlEncodedContent (
[
new KeyValuePair <string , string >("grant_type" , "client_credentials" ),
new KeyValuePair <string , string >("scope" , scope)
]);
using var httpClient = new HttpClient ();
var response = await httpClient.SendAsync (request );
response.EnsureSuccessStatusCode ();
var responseContent = await response.Content.ReadAsStringAsync ();
var tokenResponse = JObject.Parse (responseContent );
var accessToken = tokenResponse["access_token" ].ToString ();
x.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue ("Bearer" , accessToken);
Cache
Program.cs
services.AddMemoryCache ();
afficher MyClient.cs
public class MyClient (HttpClient httpClient, IMemoryCache cache) : IMyClient
{
private static readonly TimeSpan expiration = TimeSpan.FromHours (1 );
private async Task <Item []> GetItemsAsync ()
{
var uri = "items" ;
if (cache.TryGetValue (uri , out Item []? cachedResponse ))
return cachedResponse ;
var response = await httpClient.GetAsync (uri );
if (!response.IsSuccessStatusCode)
{
var errorMessage = $"url: {httpClient .BaseAddress } /{uri } , status code: {response .StatusCode } , content: {await response .Content .ReadAsStringAsync ()} " ;
return [];
}
var content = await response.Content.ReadFromJsonAsync <Item []?>();
cache.Set (uri , content , expiration );
return content;
}
GET
var items = await httpClient.GetFromJsonAsync <ItemResponse []>(uri ) ?? [];
var response = await HttpClient.GetAsync (uri , cancellationToken );
if (response.StatusCode == HttpStatusCode.???)
response.EnsureSuccessStatusCode ();
var items = (await response.Content.ReadFromJsonAsync <ItemResponse []>(cancellationToken )) ?? [];
Json Converters
public class ItemResponse
{
[JsonConverter (typeof (JsonStringEnumConverter ))] // allow (de )serialization from /to string
public ItemType ItemType { get ; set ; }
[JsonConverter (typeof(JsonStringBoolConverter))]
public bool Archived { get ; set ; }
}
public enum ItemType
{
Main,
Specific
}
afficher JsonStringBoolConverter.cs
public class JsonStringBoolConverter : JsonConverter <bool >
{
public override void Write (Utf8JsonWriter writer , bool value , JsonSerializerOptions options )
=> writer.WriteBooleanValue (value );
public override bool Read (ref Utf8JsonReader reader , Type typeToConvert , JsonSerializerOptions options )
=> reader.TokenType switch
{
JsonTokenType.True => true ,
JsonTokenType.False => false ,
JsonTokenType.String
=> bool .TryParse (reader .GetString (), out var b) ? b : throw new JsonException (),
JsonTokenType .Number
=> reader.TryGetInt64 (out var l )
? Convert .ToBoolean (l )
: reader.TryGetDouble (out var d ) && Convert .ToBoolean (d ),
_ => throw new JsonException (),
};
}
POST
public async Task <OperationStatus > CreateAsync (CreateUpdateItemQuery query )
{
ArgumentNullException.ThrowIfNull (query );
var uri = new Uri (PATH , UriKind .Relative );
var response = await httpClient.PostAsJsonAsync (uri , query );
var queryJson = new StringContent (JsonSerializer .Serialize (query), Encoding .UTF8 , "application/json" );
var response = await httpClient.PostAsync (uri , queryJson );
if (response.StatusCode == HttpStatusCode.BadRequest)
{
var validationProblemDetails =
await response.Content.ReadAsAsync <ValidationProblemDetails >(
problemJsonMediaTypeFormatters , cancellationToken );
return validationProblemDetails is null
? throw new InvalidDataException ($"{nameof (validationProblemDetails )} is null" )
: new OperationStatus (validationProblemDetails.Errors );
}
response.EnsureSuccessStatusCode ();
return new OperationStatus ();
PUT
public async Task <OperationStatus > UpdateAsync (int id , CreateUpdateItemQuery query , CancellationToken cancellationToken )
{
ArgumentNullException.ThrowIfNull (query );
var uri = new Uri ($"{Path } /{id } " , UriKind .Relative );
var queryJson = new StringContent (JsonSerializer .Serialize (query), Encoding .UTF8 , "application/json" );
var response = await httpClient.PutAsync (uri , queryJson , cancellationToken );
if (response.StatusCode == HttpStatusCode.BadRequest)
{
var validationProblemDetails =
await response.Content.ReadAsAsync <ValidationProblemDetails >(
problemJsonMediaTypeFormatters , cancellationToken );
return validationProblemDetails is null
? throw new InvalidDataException ($"{nameof (validationProblemDetails )} is null" )
: new OperationStatus (validationProblemDetails.Errors );
}
if (response.StatusCode == HttpStatusCode.NotFound)
{
return new OperationStatus ($"Item {id } has not been found." );
}
response.EnsureSuccessStatusCode ();
return new OperationStatus ();
DELETE
public async Task <OperationStatus > DeleteAsync (int id , CancellationToken cancellationToken )
{
var uri = new Uri ($"{Path } /{id } " , UriKind .Relative );
var response = await httpClient.DeleteAsync (uri , cancellationToken );
if (response.StatusCode == HttpStatusCode.NotFound)
{
return new OperationStatus ($"Item {id } has not been found." );
}
if (response.StatusCode == HttpStatusCode.InternalServerError)
{
var problemDetails = await response.Content.ReadAsAsync <ProblemDetails >(
problemJsonMediaTypeFormatters , cancellationToken );
var errorMessage = problemDetails is null || string .IsNullOrEmpty (problemDetails .Detail )
? "Unable to read the ProblemDetails error message"
: problemDetails .Detail .Replace (" See the inner exception for details." , string .Empty );
return new OperationStatus (errorMessage);
}
response.EnsureSuccessStatusCode ();
return new OperationStatus ();
ClientBase
ClientBase for
GetById
GetAll
Create
Update
Delete
ItemClient.cs
public class ItemClient :
ClientBaseWithGetAll <ItemResponse , CreateUpdateItemQuery >, IItemClient
{
protected override string Path => "item" ;
public ItemClient (HttpClient httpClient )
: base (httpClient )
{ }
}
IItemClient.cs
public interface IItemClient :
IClientBaseWithGetAll <ItemResponse , CreateUpdateItemQuery >
{ }
afficher ClientBase.cs
public abstract class ClientBase <TResponse , TCreateUpdateQuery > :
IClientBase <TResponse , TCreateUpdateQuery >
{
protected static IEnumerable<MediaTypeFormatter> problemJsonMediaTypeFormatters
=> new MediaTypeFormatter [1 ] { new ProblemJsonMediaTypeFormatter () };
protected abstract string Path { get ; }
protected HttpClient HttpClient { get ; }
protected ClientBase (HttpClient httpClient )
{
HttpClient = httpClient;
}
public async Task <OperationResult <TResponse >> GetByIdAsync (
int id ,
CancellationToken cancellationToken )
{
var uri = new Uri ($"{Path } /{id } " , UriKind .Relative );
var response = await HttpClient.GetAsync (uri , cancellationToken );
if (response.StatusCode == HttpStatusCode.NotFound)
{
return new OperationResult <TResponse >($"{typeof (TResponse )} {id } has not been found." );
}
response.EnsureSuccessStatusCode ();
var content = await response.Content.ReadAsAsync <TResponse >(cancellationToken );
return new OperationResult <TResponse >(content);
}
public async Task <OperationStatus > DeleteAsync (int id , CancellationToken cancellationToken )
{
var uri = new Uri ($"{Path } /{id } " , UriKind .Relative );
var response = await HttpClient.DeleteAsync (uri , cancellationToken );
if (response.StatusCode == HttpStatusCode.NotFound)
{
return new OperationStatus ($"{typeof (TResponse )} {id } has not been found." );
}
if (response.StatusCode == HttpStatusCode.InternalServerError)
{
var problemDetails = await response.Content.ReadAsAsync <ProblemDetails >(
problemJsonMediaTypeFormatters , cancellationToken );
var errorMessage = problemDetails is null || string .IsNullOrEmpty (problemDetails .Detail )
? "Unable to read the ProblemDetails error message"
: problemDetails .Detail .Replace (" See the inner exception for details." , string .Empty );
return new OperationStatus (errorMessage);
}
response.EnsureSuccessStatusCode ();
return new OperationStatus ();
}
public async Task <OperationStatus > CreateAsync (
TCreateUpdateQuery query ,
CancellationToken cancellationToken )
{
ArgumentNullException.ThrowIfNull (query );
var uri = new Uri (Path , UriKind .Relative );
var queryJson = new StringContent (JsonSerializer .Serialize (query), Encoding .UTF8 , "application/json" );
var response = await HttpClient.PostAsync (uri , queryJson , cancellationToken );
if (response.StatusCode == HttpStatusCode.BadRequest)
{
var validationProblemDetails =
await response.Content.ReadAsAsync <ValidationProblemDetails >(
problemJsonMediaTypeFormatters , cancellationToken );
return validationProblemDetails is null
? throw new InvalidDataException ($"{nameof (validationProblemDetails )} is null" )
: new OperationStatus (validationProblemDetails.Errors );
}
response.EnsureSuccessStatusCode ();
return new OperationStatus ();
}
public async Task <OperationStatus > UpdateAsync (
int id ,
TCreateUpdateQuery query ,
CancellationToken cancellationToken )
{
ArgumentNullException.ThrowIfNull (query );
var uri = new Uri ($"{Path } /{id } " , UriKind .Relative );
var queryJson = new StringContent (JsonSerializer .Serialize (query), Encoding .UTF8 , "application/json" );
var response = await HttpClient.PutAsync (uri , queryJson , cancellationToken );
if (response.StatusCode == HttpStatusCode.BadRequest)
{
var validationProblemDetails =
await response.Content.ReadAsAsync <ValidationProblemDetails >(
problemJsonMediaTypeFormatters , cancellationToken );
return validationProblemDetails is null
? throw new InvalidDataException ($"{nameof (validationProblemDetails )} is null" )
: new OperationStatus (validationProblemDetails.Errors );
}
if (response.StatusCode == HttpStatusCode.NotFound)
{
return new OperationStatus ($"{typeof (TResponse )} {id } has not been found." );
}
response.EnsureSuccessStatusCode ();
return new OperationStatus ();
}
}
afficher IClientBase.cs
public interface IClientBase <TResponse , TCreateUpdateQuery >
{
Task <OperationResult <TResponse >> GetByIdAsync (int id , CancellationToken cancellationToken );
Task <OperationStatus > DeleteAsync (int id , CancellationToken cancellationToken );
Task <OperationStatus > CreateAsync (
TCreateUpdateQuery query ,
CancellationToken cancellationToken );
Task <OperationStatus > UpdateAsync (
int id ,
TCreateUpdateQuery query ,
CancellationToken cancellationToken );
}
afficher ClientBaseWithGetAll.cs
public abstract class ClientBaseWithGetAll <TResponse , TCreateUpdateQuery > :
ClientBase <TResponse , TCreateUpdateQuery >
{
protected ClientBaseWithGetAll (HttpClient httpClient )
: base (httpClient )
{ }
public async Task <IReadOnlyCollection <TResponse >> GetAllAsync (
CancellationToken cancellationToken )
{
var uri = new Uri (Path , UriKind .Relative );
var response = await HttpClient.GetAsync (uri , cancellationToken );
response.EnsureSuccessStatusCode ();
var content = await response.Content
.ReadAsAsync <IReadOnlyCollection <TResponse >>(cancellationToken );
return content;
}
}
afficher IClientBaseWithGetAll.cs
public interface IClientBaseWithGetAll <TResponse , TCreateUpdateQuery > :
IClientBase <TResponse , TCreateUpdateQuery >
{
Task <IReadOnlyCollection <TResponse >> GetAllAsync (CancellationToken cancellationToken );
}
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 } " ;
}
Action Filter
UpdateMyClassFilter.cs
public class UpdateMyClassFilter : IActionFilter
{
public void OnActionExecuting (ActionExecutingContext context )
{
if (context.ActionArguments.Values.FirstOrDefault (x => x is MyClass ) is MyClass myClass )
{
// do modification on myClass
}
}
public void OnActionExecuted (ActionExecutedContext context ) { }
}
afficher EmptyResultTo204Filter.cs
public class EmptyResultTo204Filter : IActionFilter
{
public void OnActionExecuting (ActionExecutingContext context )
{ }
public void OnActionExecuted (ActionExecutedContext context )
{
if (context.Result is EmptyResult)
context.Result = new StatusCodeResult (StatusCodes .Status204NoContent );
}
}
Program.cs
builder.Services
.AddControllers (x =>
{
x .Filters .Add <MyFilter >();
});
Middleware
EnsureNoContentMiddleware.cs
public class EnsureNoContentMiddleware (RequestDelegate next)
{
public async Task InvokeAsync (HttpContext context )
{
await next(context);
if (context.Response.StatusCode == StatusCodes.Status200OK
&& context.Response.ContentLength == null
&& !context.Response.HasStarted)
{
context.Response.StatusCode = StatusCodes.Status204NoContent;
}
}
}
Program.cs
app.UseMiddleware <EnsureNoContentMiddleware >();
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;
JSON name mapping
[JsonPropertyName ("otherName" )]
public string Property1 { get ; set ; }
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 ();
Program.cs
builder.Services.AddControllers (options =>
{
options .ModelBinderProviders .Insert (0 , new CommaSeparatedModelBinderProvider ());
});
CommaSeparatedModelBinderProvider.cs
public class CommaSeparatedModelBinderProvider : IModelBinderProvider
{
public IModelBinder? GetBinder (ModelBinderProviderContext context )
{
ArgumentNullException .ThrowIfNull (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 )
{
ArgumentNullException.ThrowIfNull (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 ));
if (list is not null )
{
foreach (var splitValue in value .Split (commaSeparator ))
{
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;
}
}
FileClient.cs
await using var stream = System.IO.File.OpenRead ("/path/MyFile.txt" );
using var content = new MultipartFormDataContent
{
{ new StreamContent (stream), "file" , "MyFile.txt" }
};
var response = await HttpClient.PostAsync (uri , content , cancellationToken );
FileController.cs
[HttpPost ]
public async Task <IActionResult > ImportAsync (
[FromForm ] IFormFile file ,
CancellationToken cancellationToken )
{
using var memoryStream = new MemoryStream ();
await file.CopyToAsync (memoryStream , cancellationToken );
byte [] b = memoryStream.ToArray ();
}
Log
Add trace level for HttpClient, so you will get url and headers.
appsettings.json
{
"Logging" : {
"LogLevel" : {
"System.Net.Http.HttpClient" : "Trace"
}
}
}
To test in Visual Studio, run without the debugger.
TaskController.cs
[ApiController ]
[Route ("[controller]" )]
public class TaskController : ControllerBase
{
[HttpPost ("run" )]
[RequestTimeout (1000 )] // 1000 ms
[RequestTimeout ("OneSecondPolicy" )]
public async Task RunTaskAsync (CancellationToken cancellationToken )
{
await Task .Delay (2000 , cancellationToken );
}
}
Program.cs
builder.Services.AddRequestTimeouts ();
builder.Services.AddRequestTimeouts (options => {
options .DefaultPolicy = new RequestTimeoutPolicy { Timeout = TimeSpan .FromMilliseconds (1000 ) };
options.AddPolicy ("OneSecondPolicy" , TimeSpan .FromSeconds (1 ));
});
app.UseRequestTimeouts ();
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é.