« Asp.net web api 2 » : différence entre les versions
(11 versions intermédiaires par le même utilisateur non affichées) | |||
Ligne 267 : | Ligne 267 : | ||
= Client = | = Client = | ||
== GET == | == GET == | ||
Use [[Asp.net_web_api_2#JsonConverter|JsonConverter]] to build a custom uri with parameters. | |||
<kode lang='cs'> | <kode lang='cs'> | ||
var httpClient = new HttpClient(); | var httpClient = new HttpClient(); | ||
Ligne 276 : | Ligne 277 : | ||
string content = httpClient.GetAsync(url).Result.Content.ReadAsStringAsync().Result; | string content = httpClient.GetAsync(url).Result.Content.ReadAsStringAsync().Result; | ||
// requête en 2 | // requête en 2 temps avec test du status code | ||
HttpResponseMessage response = await httpClient.GetAsync(url); | HttpResponseMessage response = await httpClient.GetAsync(url); | ||
if (response.IsSuccessStatusCode) | if (response.IsSuccessStatusCode) | ||
Ligne 294 : | Ligne 295 : | ||
// disponible dans le paquet NuGet Microsoft.AspNet.WebApi.Client | // disponible dans le paquet NuGet Microsoft.AspNet.WebApi.Client | ||
// et le using System.Net.Http; | // et le using System.Net.Http; | ||
MyDto myDto = await response.Content.ReadAsAsync<MyDto>(); | |||
} | } | ||
else | else | ||
{ | { | ||
// retourne un Task<T> avec un contenu null | // retourne un Task<T> avec un contenu null | ||
return await Task.FromResult< | return await Task.FromResult<MyDto>(null); | ||
} | } | ||
</kode> | </kode> | ||
Ligne 344 : | Ligne 345 : | ||
HttpResponseMessage responseMessage = await httpClient.SendAsync(request); | HttpResponseMessage responseMessage = await httpClient.SendAsync(request); | ||
string result = await responseMessage.Content.ReadAsStringAsync(); | string result = await responseMessage.Content.ReadAsStringAsync(); | ||
</kode> | |||
== Query Builder == | |||
<kode lang='cs'> | |||
var queryString = HttpUtility.ParseQueryString(string.Empty); | |||
queryString.Add("key", "value"); | |||
var uri = new Uri($"http://api.domain.net/query?{queryString}", UriKind.Relative); | |||
// http://api.domain.net/query?key=value | |||
</kode> | </kode> | ||
Ligne 361 : | Ligne 370 : | ||
</kode> | </kode> | ||
= Dependencies Injection = | = [https://docs.microsoft.com/en-us/aspnet/web-api/overview/advanced/dependency-injection Dependencies Injection] = | ||
* Install | * Install the {{boxx|DryIoc.WebApi}} nuget package | ||
<filebox fn='Global.asax.cs'> | <filebox fn='Global.asax.cs'> | ||
public class WebApiApplication : | public class WebApiApplication : HttpApplication | ||
{ | { | ||
protected void Application_Start() | protected void Application_Start() | ||
Ligne 371 : | Ligne 380 : | ||
</filebox> | </filebox> | ||
<filebox fn='DryIocConfig.cs'> | <filebox fn='App_Start/DryIocConfig.cs'> | ||
public static | public static class DryIocConfig | ||
{ | { | ||
var container = new Container(); | public static void Register(HttpConfiguration config) | ||
{ | |||
var container = new Container(); | |||
container.Register<IMyService, MyService>(); | |||
container.WithWebApi(config); | |||
} | |||
} | } | ||
</filebox> | </filebox> | ||
Ligne 486 : | Ligne 498 : | ||
</filebox> | </filebox> | ||
Url: {{boxx|<nowiki>http://localhost:{port}/swagger</nowiki>}} | * Url: {{boxx|<nowiki>http://localhost:{port}/swagger</nowiki>}} | ||
* Right-click on the project → Properties → Web → Start Action → Specific Page → {{boxx|/swagger}} | |||
= Visual Studio et un projet Web API = | = Visual Studio et un projet Web API = |
Dernière version du 2 juin 2022 à 15:54
Liens
Web API vs MVC
MVC est designé pour des applications web standard qui servent des pages HTML.
Web API est designé pour des applications REST qui servent des objets sérialisés (JSON, XML).
Routing
App_Start/WebApiConfig.cs |
public static void Register(HttpConfiguration config) { config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{action}", defaults: new { action = RouteParameter.Optional } ); |
Global.asax.cs |
public class WebApiApplication : HttpApplication { protected void Application_Start() { GlobalConfiguration.Configure(WebApiConfig.Register); } |
Api Controller
GET
TestController.cs |
[ApiVersion("1")] [RoutePrefix("api/v{version:apiVersion}/test")] // prefix pour toutes les méthodes du controller public class TestController : ApiController { // api/v1/test/result/{query} [HttpGet] // force GET (optinal, GET by default) [Route("result/{query}")] // route avec 1 paramètre public IHttpActionResult Get(string query) { } // api/v1/test/result/{query1}/{query2} [Route("result/{query1}/{query2}")] // route avec 2 paramètres public IHttpActionResult Get(string query1, string query2) { } // api/v1/test/result/{query}/{optionalQuery?} [Route("result/{query}/{optionalQuery?}")] // route avec 1 paramètre optionnel public IHttpActionResult Get(string query, string optionalQuery = null) { } // api/v1/test?query=text [Route("")] public IHttpActionResult Get([FromUri]string query) { } // api/v1/test/result?Prop1=1&Prop2=2 [Route("")] public IHttpActionResult Get([FromUri]ComplexType ct) { return this.Ok(ct); // { "Prop1": 1, "Prop2": 2 } } // api/v1/test?cts[0].Prop1=1&cts[0].Prop2=2&cts[1].Prop1=3&cts[1].Prop2=4 [Route("")] public IHttpActionResult Get([FromUri]IEnumerable<ComplexType> cts) { return this.Ok(cts); // [ // { "Prop1": 1, "Prop2": 2 }, // { "Prop1": 3, "Prop2": 4 } // ] } // api/other, use ~ to overwrite route prefix [Route("~/api/other")] public IHttpActionResult Get() { } |
POST
TestController.cs |
// api/v1/test // item is passed in Body [Post] [Route("")] [ResponseType(typeof(Item))] public IHttpActionResult Post(Item item) { if (item == null || item.Id == 0 || string.IsNullOrWhiteSpace(item.Name)) { return this.BadRequest(); } if (items.Select(i => i.Id).Contains(item.Id)) { return this.Conflict(); } else { return this.Created( this.Request.RequestUri + $"?id={item.Id}", item); } |
Content Negociation
Permet de spécifier sous quelle forme on souhaite la réponse.
Le HTTP Header Accept permet de spécifier le format attendu.
curl http://localhost:111/api/values -H "Accept: application/xml" |
L'extension pour Firefox RESTED permet de tester les requêtes. |
Global.asax.cs |
protected void Application_Start() { // return json by default (ajouter à la fin de la méthode) GlobalConfiguration.Configuration.Formatters.Clear(); GlobalConfiguration.Configuration.Formatters.Add(new JsonMediaTypeFormatter()); |
App_Start/WebApiConfig.cs |
// return html by default instead of xml config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html")); |
Header
MyClient.cs |
var request = new HttpRequestMessage(HttpMethod.Post, uri); request.Headers.Add("HeaderName", headerValues); request.Content = new StringContent(JsonConvert.SerializeObject(myObject)); var response = await this.httpClient.SendAsync(request, cancellationToken); response.EnsureSuccessStatusCode(); |
MyController.cs |
[HttpGet] [Route("route")] [OpenApiTag(ControllerName)] [SwaggerResponse(HttpStatusCode.OK, typeof(bool))] [SwaggerResponse(HttpStatusCode.BadRequest, typeof(void))] public async Task<IHttpActionResult> TestAsync([FromUri]SomeQuery someQuery, CancellationToken cancellationToken) { if (this.Request.Headers.TryGetValues("HeaderName", out var headerValues)) {} |
JsonConverter
Allow to customize the generated JSON.
ItemClient.cs |
public async Task<IReadOnlyList<Item>> GetByIdsAsync(IReadOnlyCollection<int> ids, CancellationToken cancellationToken) { var serializedIds = QueryStringSerializer.Serialize(ids, new ListJsonConverter()); var uri = new Uri($"items?ids={serializedIds}", UriKind.Relative); |
Model Binder
Permet de redéfinir la manière dont sera déserialisé l'url.
TestController.cs |
// association 1, fonctionne seulement si ComplexType est passé en paramètre // api/test?ct=1,2 public IHttpActionResult Get([ModelBinder(typeof(ComplexTypeModelBinder))] ComplexType ct) { } // api/test?cts[0]=1,2&cts[1]=3,4 public IHttpActionResult Get([FromUri] IEnumerable<ComplexType> cts) { } // api/test?ComplexTypes[0]=1,2&ComplexTypes[1]=3,4 public IHttpActionResult Get([FromUri] Container container) { } |
ComplexTypeModelBinder.cs |
using System.Web.Http.ModelBinding; // System.Web.Http.ModelBinding.IModelBinder public class ComplexTypeModelBinder : IModelBinder { public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext) { if (bindingContext.ModelType != typeof(ComplexType)) { return false; } ValueProviderResult val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); if (val == null) { return false; } // doesn't seem useful if (!(val.RawValue is string key)) { bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Wrong value type"); return false; } var splittedValues = valueProviderResult.AttemptedValue.Split(','); if (splittedValues.Length != 2) { bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Cannot convert value to ComplexType"); return false; } var result = new ComplexType { Prop1 = int.Parse(values[0]), Prop2 = int.Parse(values[1]) }; bindingContext.Model = result; return true; } } |
ComplexType.cs |
// association 2, fonctionne si ComplexType est passé en paramètre mais aussi s'il est un sous-élément du paramètre [ModelBinder(typeof(ComplexTypeModelBinder))] public class ComplexType { public int Prop1 { get; set; } public int Prop2 { get; set; } } public class Container { public IEnumerable<ComplexType> ComplexTypes { get; set; } } |
WebApiConfig.cs |
public static void Register(HttpConfiguration config) { // association 3, fonctionne seulement si ComplexType est passé en paramètre // la méthode du controller ne doit pas contenir FromUri config.ParameterBindingRules.Add( typeof(ComplexType), descriptor => descriptor.BindWithModelBinding(new ComplexTypeModelBinder())); // association 4, fonctionne si ComplexType est passé en paramètre mais aussi s'il est un sous-élément du paramètre // la méthode du controller doit contenir FromUri config.Services.Insert(typeof(ModelBinderProvider), 0, new ComplexTypeModelBinderProvider()); } |
ComplexTypeModelBinderProvider.cs |
public sealed class ComplexTypeModelBinderProvider : ModelBinderProvider { public override IModelBinder GetBinder(HttpConfiguration configuration, Type modelType) { return modelType == typeof(ComplexType) ? new ComplexTypeModelBinder() : null; } } |
Documentation
Project Properties → Build → Output → cocher XML Documentation file → App_Data\[ProjectName].xml
Areas/HelpPage/App_Start/HelpPageConfig.cs |
public static class HelpPageConfig { public static void Register(HttpConfiguration config) { config.SetDocumentationProvider(new XmlDocumentationProvider(HttpContext.Current.Server.MapPath("~/App_Data/[ProjectName].xml"))); |
Client
GET
Use JsonConverter to build a custom uri with parameters.
var httpClient = new HttpClient(); // demande explicitement du xml ("application/xml") ou du json ("application/json") httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); // requête en une ligne utilisant Result, ce qui brise l'async string content = httpClient.GetAsync(url).Result.Content.ReadAsStringAsync().Result; // requête en 2 temps avec test du status code HttpResponseMessage response = await httpClient.GetAsync(url); if (response.IsSuccessStatusCode) { // XML var doc = XDocument.Load(await response.Content.ReadAsStreamAsync()); var ns = (XNamespace)"http://schemas.datacontract.org/2004/07/WebAPITest.Models"; foreach (var name in doc.Descendants(ns + "Name")) { Console.WriteLine(name.Value); } // récupération du contenu comme un string string content = await response.Content.ReadAsStringAsync(); // désérialisation du contenu // ReadAsAsync<T> nécessite l'assembly System.Net.Http.Formatting // disponible dans le paquet NuGet Microsoft.AspNet.WebApi.Client // et le using System.Net.Http; MyDto myDto = await response.Content.ReadAsAsync<MyDto>(); } else { // retourne un Task<T> avec un contenu null return await Task.FromResult<MyDto>(null); } |
<ArrayOfPerson xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/WebAPITest.Models"> <Person> <Name>John</Name> </Person> <Person> <Name>Billy</Name> </Person> </ArrayOfPerson> |
POST
// content dans l'url var content = new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("clé", "valeur") }); // content dans le body var content = new StringContent( new JavaScriptSerializer().Serialize(myObject), Encoding.UTF8, "application/json"); HttpResponseMessage responseMessage = await httpClient.PostAsync(url, content); string result = await responseMessage.Content.ReadAsStringAsync(); |
Token
// GET var request = new HttpRequestMessage(HttpMethod.Get, url); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); // POST var request = new HttpRequestMessage(HttpMethod.Post, url); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); request.Content = content; HttpResponseMessage responseMessage = await httpClient.SendAsync(request); string result = await responseMessage.Content.ReadAsStringAsync(); |
Query Builder
var queryString = HttpUtility.ParseQueryString(string.Empty); queryString.Add("key", "value"); var uri = new Uri($"http://api.domain.net/query?{queryString}", UriKind.Relative); // http://api.domain.net/query?key=value |
HttpResponseException
By default, most exceptions are translated into an HTTP response with status code 500, Internal Server Error. |
// HttpResponseException permet de spécifier le code d'erreur, ici 404 throw new HttpResponseException(HttpStatusCode.NotFound); // spécifie en plus le Content et la ReasonPhrase var responseMessage = new HttpResponseMessage(HttpStatusCode.NotFound) { Content = new StringContent("Error content."), ReasonPhrase = "Error reason." } throw new HttpResponseException(responseMessage); |
Dependencies Injection
- Install the DryIoc.WebApi nuget package
Global.asax.cs |
public class WebApiApplication : HttpApplication { protected void Application_Start() { GlobalConfiguration.Configure(DryIocConfig.Register); |
App_Start/DryIocConfig.cs |
public static class DryIocConfig { public static void Register(HttpConfiguration config) { var container = new Container(); container.Register<IMyService, MyService>(); container.WithWebApi(config); } } |
Loguer les exceptions non-gérées
Nuget: Microsoft.AspNet.WebApi.Core |
App_Start\WebApiConfig.cs |
public static class WebApiConfig { public static void Register(HttpConfiguration config) { /* ... */ // plusieurs ExceptionLogger peuvent être ajoutés, permet juste de loguer config.Services.Add(typeof(IExceptionLogger), new MyExceptionLogger()); // un seul ExceptionHandler, permet de modifier la réponse config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler()); |
MyExceptionLogger.cs |
public class MyExceptionLogger : ExceptionLogger { public override void Log(ExceptionLoggerContext context) { // log it base.Log(context); |
GlobalExceptionHandler.cs |
public class GlobalExceptionHandler : ExceptionHandler { public override void Handle(ExceptionHandlerContext context) { // réécrit le message d'erreur context.Result = new TextErrorResult { RequestMessage = context.ExceptionContext.Request, Content = context.Exception.GetAllMessages() }; private class TextErrorResult : IHttpActionResult { public HttpRequestMessage RequestMessage { get; set; } public string Content { get; set; } public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken) { var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); response.Content = new StringContent(Content); response.RequestMessage = RequestMessage; return Task.FromResult(response); |
Authorize
Pour accéder à une méthode Authorize, il faut passer un token d'accès. |
MyController.cs |
[Authorize] [HttpGet] [Route("userinfo")] public string UserInfo() { string name = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Name).Value; string givenName = ClaimsPrincipal.Current.FindFirst(ClaimTypes.GivenName).Value; string surname = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Surname).Value; string email = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Email); string upn = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn).Value; // user principal name string scope = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/scope").Value; string objectId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value; // using Microsoft.AspNet.Identity; // packaget NuGet Microsoft ASP.NET Identity Core string userId = User.Identity.GetUserId(); } |
Cross-Origin Requests
Permet à d'autres sites d'appeler mon API Web, contournant ainsi la same-origin policy.
Swagger / NSwag
Install Nuget packages: Microsoft.Owin.Host.SystemWeb NSwag.AspNet.Owin
Global.asax.cs |
protected void Application_Start() { RouteTable.Routes.MapOwinPath("swagger", app => { app.UseSwaggerReDoc(typeof(WebApiApplication).Assembly, s => { s.GeneratorSettings.DefaultUrlTemplate = "api/{controller}/{id}"; s.MiddlewareBasePath = "/swagger"; }); }); |
Web.config |
<configuration> <appSettings> <add key="owin:AutomaticAppStartup" value="false" /> </appSettings> <system.webServer> <!-- choice 1: Pipe all request to the .NET pipeline --> <modules runAllManagedModulesForAllRequests="true" /> <!-- choice 2: Pipe only the Swagger request to the specific middlewares --> <handlers> <add name="NSwag" path="swagger" verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" /> </handlers> </system.webServer> |
- Url: http://localhost:{port}/swagger
- Right-click on the project → Properties → Web → Start Action → Specific Page → /swagger
Visual Studio et un projet Web API
- New Project → ASP.NET Web Application → Template: Empty → cocher Web API
- Clique-droit sur le dossier Controllers → Add Controller → Web API 2 Controller - Empty
Erreurs
Les appels consécutifs à plusieurs Web API échappent plusieurs fois les caractères
Les appels consécutifs à GetStringAsync échappent autant de fois les caractères.
Utiliser plutôt GetAsync.
// autre solution string json = await httpClient.GetStringAsync(url); HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK); response.Content = new StringContent(json, Encoding.UTF8, "application/json"); // retourner response au lieu de json |
The file or directory does not exist on the server
Le verbe utilisé n'est pas autorisé sur le serveur. Changer la configuration du serveur.
Documents\IISExpress\config\applicationhost.xml |
<add name="ExtensionlessUrl-Integrated-4.0" path="*." verb="GET,HEAD,POST,DEBUG" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" /> |
VS utilise [Dossier solution]\.vs\config\applicationhost.xml en surcharge du fichier de config global. |