Skip to content
This repository has been archived by the owner on Jan 10, 2023. It is now read-only.

Commit

Permalink
Add a web API
Browse files Browse the repository at this point in the history
Supports JSON, XML and CSV output.
Supports content negotiation via Accept HTTP header.
Supports content negotiation via 'file extensions'.
  • Loading branch information
langsamu committed Jun 13, 2020
1 parent c2c4336 commit 5a29187
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 5 deletions.
14 changes: 14 additions & 0 deletions WebApplication1/Controllers/ApiController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace WebApplication1
{
using Microsoft.AspNetCore.Mvc;

[ApiController]
public class ApiController : ControllerBase
{
[HttpGet("api")]
[HttpGet("api.{format}")]
[FormatFilter]
public IActionResult Get([FromQuery] ApiParameters p) =>
this.Ok(p);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace WebApplication1.Controllers
namespace WebApplication1
{
using System;
using System.Linq;
Expand All @@ -8,8 +8,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;

[Route("")]
public class Default : Controller
public class DefaultController : Controller
{
private const int timeout = 1000;

Expand Down Expand Up @@ -46,7 +45,7 @@ public IActionResult Error()

if (handler.Path.StartsWith("/async/") && handler.Error is OperationCanceledException)
{
return this.BadRequest($"Couldn't generate {handler.Path[7..]} primes in {timeout}ms. Try less.");
return this.BadRequest($"Could not generate {handler.Path[7..]} primes in {timeout}ms. Try less.");
}
else
{
Expand Down
51 changes: 51 additions & 0 deletions WebApplication1/Formatters/CsvFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
namespace WebApplication1
{
using System;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ClassLibrary1;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Net.Http.Headers;

internal class CsvFormatter : TableFormatter
{
internal CsvFormatter()
: base(new MediaTypeHeaderValue("text/csv"))
{
}

protected override async Task WriteResponseBodyAsync(MultiplicationTable table, CancellationToken cancellationToken, OutputFormatterWriteContext context, Encoding encoding)
{
using var writer = context.WriterFactory(context.HttpContext.Response.Body, encoding);

try
{
await foreach (var row in table.WithCancellation(cancellationToken))
{
var fieldNumber = 0;
await foreach (var cell in row)
{
if (fieldNumber++ > 0)
{
await writer.WriteAsync(",");
}

await writer.WriteAsync(cell?.ToString());
}

await writer.WriteLineAsync();
}
}
catch (OperationCanceledException)
{
await writer.WriteAsync("# Could not multiply primes in time limit. Try less.");

context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
}

await writer.FlushAsync();
}
}
}
62 changes: 62 additions & 0 deletions WebApplication1/Formatters/JsonFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
namespace WebApplication1
{
using System;
using System.Net;
using System.Net.Mime;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using ClassLibrary1;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Net.Http.Headers;

internal class JsonFormatter : TableFormatter
{
internal JsonFormatter()
: base(new MediaTypeHeaderValue(MediaTypeNames.Application.Json))
{
}

protected override async Task WriteResponseBodyAsync(MultiplicationTable table, CancellationToken cancellationToken, OutputFormatterWriteContext context, Encoding encoding)
{
using var writer = new Utf8JsonWriter(context.HttpContext.Response.Body);

try
{
writer.WriteStartArray();

await foreach (var row in table.WithCancellation(cancellationToken))
{
writer.WriteStartArray();

await foreach (var cell in row)
{
if (cell.HasValue)
{
writer.WriteNumberValue(cell.Value);
}
else
{
writer.WriteNullValue();
}
}

writer.WriteEndArray();
}

writer.WriteEndArray();
}
catch (OperationCanceledException)
{
writer.WriteStartObject();
writer.WriteString("error", "Could not multiply primes in time limit. Try less.");
writer.WriteEndObject();

context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
}

await writer.FlushAsync();
}
}
}
50 changes: 50 additions & 0 deletions WebApplication1/Formatters/TableFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
namespace WebApplication1
{
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ClassLibrary1;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Net.Http.Headers;

internal abstract class TableFormatter : TextOutputFormatter
{
protected TableFormatter(MediaTypeHeaderValue mimeType)
{
this.SupportedMediaTypes.Add(mimeType);
this.SupportedEncodings.Add(new UTF8Encoding(false));
}

public sealed override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding encoding)
{
var data = (ApiParameters)context.Object;

var options = data.ThrowOnCancel ?
PrimeGeneratorOptions.ThrowOnCancel :
PrimeGeneratorOptions.None;

var table = new MultiplicationTable(data.Count, options);

var cancellationToken =
CancellationTokenSource.CreateLinkedTokenSource(
data.cancellationToken,
context.HttpContext.RequestAborted).Token;

if (data.Timeout.HasValue)
{
cancellationToken =
CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken,
new CancellationTokenSource(data.Timeout.Value).Token).Token;
}

await this.WriteResponseBodyAsync(table, cancellationToken, context, encoding);
}

protected abstract Task WriteResponseBodyAsync(MultiplicationTable table, CancellationToken cancellationToken, OutputFormatterWriteContext context, Encoding encoding);

protected sealed override bool CanWriteType(Type type) =>
typeof(ApiParameters).IsAssignableFrom(type);
}
}
53 changes: 53 additions & 0 deletions WebApplication1/Formatters/XmlFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
namespace WebApplication1
{
using System;
using System.Net;
using System.Net.Mime;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using ClassLibrary1;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Net.Http.Headers;

internal class XmlFormatter : TableFormatter
{
internal XmlFormatter()
: base(new MediaTypeHeaderValue(MediaTypeNames.Text.Xml))
{
}

protected override async Task WriteResponseBodyAsync(MultiplicationTable table, CancellationToken cancellationToken, OutputFormatterWriteContext context, Encoding encoding)
{
using var writer = XmlWriter.Create(context.WriterFactory(context.HttpContext.Response.Body, encoding), new XmlWriterSettings { Async = true });

try
{
await writer.WriteStartElementAsync(null, "table", null);

await foreach (var row in table.WithCancellation(cancellationToken))
{
await writer.WriteStartElementAsync(null, "row", null);

await foreach (var cell in row)
{
await writer.WriteElementStringAsync(null, "cell", null, cell.ToString());
}

await writer.WriteEndElementAsync();
}

await writer.WriteEndElementAsync();
}
catch (OperationCanceledException)
{
await writer.WriteProcessingInstructionAsync("error", "Could not multiply primes in time limit. Try less.");

context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
}

await writer.FlushAsync();
}
}
}
18 changes: 18 additions & 0 deletions WebApplication1/Models/ApiParameters.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace WebApplication1
{
using System.ComponentModel.DataAnnotations;
using System.Threading;

public class ApiParameters
{
[Range(1, int.MaxValue)]
public int Count { get; set; }

[Range(1, int.MaxValue)]
public int? Timeout { get; set; }

public bool ThrowOnCancel { get; set; }

public CancellationToken cancellationToken { get; set; }
}
}
14 changes: 13 additions & 1 deletion WebApplication1/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace WebApplication1
{
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
Expand All @@ -17,7 +18,18 @@ public Startup(IConfiguration configuration)

public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddMvc(this.ConfigureMvc);
}

private void ConfigureMvc(MvcOptions mvc)
{
mvc.OutputFormatters.Insert(0, new CsvFormatter());
mvc.OutputFormatters.Insert(0, new XmlFormatter());
mvc.OutputFormatters.Insert(0, new JsonFormatter());

mvc.FormatterMappings.SetMediaTypeMappingForFormat("csv", "text/csv");
mvc.FormatterMappings.SetMediaTypeMappingForFormat("xml", "text/xml");
mvc.FormatterMappings.SetMediaTypeMappingForFormat("json", "application/json");
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
Expand Down

0 comments on commit 5a29187

Please sign in to comment.