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
[ApiVersion ("1" )]
[ApiController ]
[Route ("[controller]" )]
[Produces ("application/json" )]
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 > 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 ();
}
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 );
}
}
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
CORS
Programs.cs
app.UseCors (x => x .SetIsOriginAllowed (origin => true ));
app.UseCors (x => x .WithOrigins ("https://localhost:4200" ));
Client
Client/ItemClient.cs
public class ItemClient : IItemClient
{
private const string PATH = "item" ;
private const string APIVERSION = "1" ;
protected static IEnumerable<MediaTypeFormatter> problemJsonMediaTypeFormatters
=> new MediaTypeFormatter [1 ] { new ProblemJsonMediaTypeFormatter () };
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 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 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
Program.cs
services
.AddHttpClient <IMyClient , MyClient >(x =>
{
var authenticationString = $"{username } :{password } " ;
var base64EncodedAuthenticationString = Convert .ToBase64String (Encoding .ASCII .GetBytes (authenticationString ));
x.BaseAddress = new Uri ("https://www.address.net" );
x.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue ("Basic" , base64EncodedAuthenticationString);
x.DefaultRequestHeaders.Add ("User-Agent" , "My App" );
})
.ConfigurePrimaryHttpMessageHandler (() => new HttpClientHandler
{
Proxy = new WebProxy
{
Address = new Uri ($"http://localhost:9000" ),
BypassProxyOnLocal = false ,
UseDefaultCredentials = false ,
},
});
GET
ReadAsAsync
public async Task <OperationResult <IReadOnlyCollection <ItemResponse >>> GetAsync (FetchItemQuery 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);
}
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 >($"Item {id } has not been found." );
}
response.EnsureSuccessStatusCode ();
var item = await response.Content.ReadAsAsync <ItemResponse >(cancellationToken );
return new OperationResult <ItemResponse >(item);
}
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 <OperationStatus > CreateAsync (CreateUpdateItemQuery query )
{
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 );
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 } " ;
}
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 ();
}
Kebab case
Program.cs
builder.Services
.AddControllers (x =>
x .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 ();
}
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é.