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 : System.Web.HttpApplication
{
protected void Application_Start()
{
GlobalConfiguration.Configure(WebApiConfig.Register);
}
|
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
[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"));
|
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))
{}
|
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);
|
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
|
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 temsp 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;
JObject content = await response.Content.ReadAsAsync<JObject>();
}
else
{
// retourne un Task<T> avec un contenu null
return await Task.FromResult<JObject>(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();
|
|
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 nuget packages: DryIoc DryIoc.WebApi
Global.asax.cs
|
public class WebApiApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
GlobalConfiguration.Configure(DryIocConfig.Register);
|
DryIocConfig.cs
|
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();
}
|
Permet à d'autres sites d'appeler mon API Web, contournant ainsi la same-origin policy.
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 à 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. |