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);
}
|
GET
TestController.cs
|
[ApiVersion("1")]
[RoutePrefix("api/v{version:apiVersion}/test")]
public class TestController : ApiController
{
[HttpGet]
[Route("result/{query}")]
public IHttpActionResult Get(string query) { }
[Route("result/{query1}/{query2}")]
public IHttpActionResult Get(string query1, string query2) { }
[Route("result/{query}/{optionalQuery?}")]
public IHttpActionResult Get(string query, string optionalQuery = null) { }
[Route("")]
public IHttpActionResult Get([FromUri]string query) { }
[Route("")]
public IHttpActionResult Get([FromUri]ComplexType ct)
{
return this.Ok(ct);
}
[Route("")]
public IHttpActionResult Get([FromUri]IEnumerable<ComplexType> cts)
{
return this.Ok(cts);
}
[Route("~/api/other")]
public IHttpActionResult Get()
{ }
|
POST
TestController.cs
|
[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()
{
GlobalConfiguration.Configuration.Formatters.Clear();
GlobalConfiguration.Configuration.Formatters.Add(new JsonMediaTypeFormatter());
|
App_Start/WebApiConfig.cs
|
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
|
public IHttpActionResult Get([ModelBinder(typeof(ComplexTypeModelBinder))] ComplexType ct) { }
public IHttpActionResult Get([FromUri] IEnumerable<ComplexType> cts) { }
public IHttpActionResult Get([FromUri] Container container) { }
|
afficherComplexTypeModelBinder.cs
|
using System.Web.Http.ModelBinding;
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;
}
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
|
[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)
{
config.ParameterBindingRules.Add(
typeof(ComplexType),
descriptor => descriptor.BindWithModelBinding(new ComplexTypeModelBinder()));
config.Services.Insert(typeof(ModelBinderProvider), 0, new ComplexTypeModelBinderProvider());
}
|
afficherComplexTypeModelBinderProvider.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();
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
string content = httpClient.GetAsync(url).Result.Content.ReadAsStringAsync().Result;
HttpResponseMessage response = await httpClient.GetAsync(url);
if (response.IsSuccessStatusCode)
{
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);
}
string content = await response.Content.ReadAsStringAsync();
MyDto myDto = await response.Content.ReadAsAsync<MyDto>();
}
else
{
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
|
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("clé", "valeur")
});
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
|
var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
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);
|
|
By default, most exceptions are translated into an HTTP response with status code 500, Internal Server Error. |
|
throw new HttpResponseException(HttpStatusCode.NotFound);
var responseMessage = new HttpResponseMessage(HttpStatusCode.NotFound)
{
Content = new StringContent("Error content."),
ReasonPhrase = "Error reason."
}
throw new HttpResponseException(responseMessage);
|
- 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)
{
config.Services.Add(typeof(IExceptionLogger), new MyExceptionLogger());
config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler());
|
MyExceptionLogger.cs
|
public class MyExceptionLogger : ExceptionLogger
{
public override void Log(ExceptionLoggerContext context)
{
base.Log(context);
|
GlobalExceptionHandler.cs
|
public class GlobalExceptionHandler : ExceptionHandler
{
public override void Handle(ExceptionHandlerContext context)
{
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;
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;
string userId = User.Identity.GetUserId();
}
|
Permet à d'autres sites d'appeler mon API Web, contournant ainsi la same-origin policy.
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>
<modules runAllManagedModulesForAllRequests="true" />
<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 à GetStringAsync échappent autant de fois les caractères.
Utiliser plutôt GetAsync.
|
string json = await httpClient.GetStringAsync(url);
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK);
response.Content = new StringContent(json, Encoding.UTF8, "application/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. |