Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

opt into JsonApi per webapi endpoint #187

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions Saule/Http/JsonApiAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Net.Http;
using System.Web.Http.Filters;

namespace Saule.Http
{
/// <summary>
/// An optional attribute that can be used to opt an api into returning a JsonApi response.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public sealed class JsonApiAttribute : ActionFilterAttribute
{
/// <summary>
/// See base class documentation.
/// </summary>
/// <param name="context">The action context.</param>
public override void OnActionExecuted(HttpActionExecutedContext context)
{
var config = new JsonApiConfiguration();
JsonApiProcessor.ProcessRequest(context.Request, context.Response, config, requiresMediaType: false);

if (context.Exception != null)
{
return;
}

var responseContent = context.Response.Content as ObjectContent;
if (responseContent == null)
{
return;
}

var formatter = new JsonApiMediaTypeFormatter(context.Request, config);

context.Response.Content = new ObjectContent(responseContent.ObjectType, responseContent.Value, formatter);
}
}
}
43 changes: 43 additions & 0 deletions Saule/Http/JsonApiProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Linq;
using System.Net;
using System.Net.Http;
using Saule.Serialization;

namespace Saule.Http
{
/// <summary>
/// Processes JSON API responses
/// </summary>
internal static class JsonApiProcessor
{
internal static void ProcessRequest(HttpRequestMessage request, HttpResponseMessage response, JsonApiConfiguration config, bool requiresMediaType)
{
var hasMediaType = request.Headers.Accept.Any(x => x.MediaType == Constants.MediaType);

var statusCode = (int)response.StatusCode;
if ((requiresMediaType && !hasMediaType) || (statusCode >= 400 && statusCode < 500))
{
// probably malformed request or not found
return;
}

var value = response.Content as ObjectContent;

if (config == null)
{
config = new JsonApiConfiguration();
}

var content = PreprocessingDelegatingHandler.PreprocessRequest(value?.Value, request, config);

if (content.ErrorContent != null)
{
response.StatusCode = ApiError.IsClientError(content.ErrorContent)
? HttpStatusCode.BadRequest
: HttpStatusCode.InternalServerError;
}

request.Properties.Add(Constants.PropertyNames.PreprocessResult, content);
}
}
}
21 changes: 1 addition & 20 deletions Saule/Http/PreprocessingDelegatingHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,27 +69,8 @@ internal static PreprocessResult PreprocessRequest(
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var result = await base.SendAsync(request, cancellationToken);
var hasMediaType = request.Headers.Accept.Any(x => x.MediaType == Constants.MediaType);

var statusCode = (int)result.StatusCode;
if (!hasMediaType || (statusCode >= 400 && statusCode < 500))
{
// probably malformed request or not found
return result;
}

var value = result.Content as ObjectContent;

var content = PreprocessRequest(value?.Value, request, _config);

if (content.ErrorContent != null)
{
result.StatusCode = ApiError.IsClientError(content.ErrorContent)
? HttpStatusCode.BadRequest
: HttpStatusCode.InternalServerError;
}

request.Properties.Add(Constants.PropertyNames.PreprocessResult, content);
JsonApiProcessor.ProcessRequest(request, result, _config, requiresMediaType: true);

return result;
}
Expand Down
2 changes: 2 additions & 0 deletions Saule/Saule.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
<ItemGroup>
<Compile Include="ApiResource.cs" />
<Compile Include="Http\HandlesQueryAttribute.cs" />
<Compile Include="Http\JsonApiAttribute.cs" />
<Compile Include="Http\JsonApiProcessor.cs" />
<Compile Include="Http\JsonApiQueryValueProvider.cs" />
<Compile Include="Http\JsonApiQueryValueProviderFactory.cs" />
<Compile Include="JsonApiSerializerOfT.cs" />
Expand Down
10 changes: 10 additions & 0 deletions Tests/Controllers/CompaniesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ public IEnumerable<Company> GetCompanies()
return Get.Companies(100);
}

[HttpGet]
[JsonApi]
[Paginated(PerPage = 12)]
[Route("v2/companies")]
[ReturnsResource(typeof(CompanyResource))]
public IEnumerable<Company> GetCompaniesV2()
{
return Get.Companies(100);
}

[HttpGet]
[Paginated(PerPage = 12)]
[Route("companies/querypagesize")]
Expand Down
1 change: 0 additions & 1 deletion Tests/Helpers/ObsoleteSetupJsonApiServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ public ObsoleteSetupJsonApiServer()
internal ObsoleteSetupJsonApiServer(JsonApiMediaTypeFormatter formatter)
{
var config = new HttpConfiguration();
config.Formatters.Clear();
config.Formatters.Add(formatter);
config.MapHttpAttributeRoutes(new DefaultDirectRouteProvider());

Expand Down
19 changes: 17 additions & 2 deletions Tests/Integration/ContentNegotiationTests.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Saule;
using Saule.Http;
using Tests.Helpers;
Expand All @@ -26,6 +24,23 @@ public ObsoleteSetup(ObsoleteSetupJsonApiServer server)
_server = server;
}

[Fact(DisplayName = "Servers MUST not respond with json api unless requested Content-Type is 'application/vnd.api+json' or action filter is set to 'JsonApiAttribute'")]
public async Task MustNotReturnJsonApiResponse()
{
var target = _server.GetClient();

var result = await target.GetAsync("api/companies/");
Assert.Equal("application/vnd.api+json", result.Content.Headers.ContentType.MediaType);

target.DefaultRequestHeaders.Clear();

result = await target.GetAsync("api/companies/");
Assert.Equal("application/json", result.Content.Headers.ContentType.MediaType);

result = await target.GetAsync("api/v2/companies/");
Assert.Equal("application/vnd.api+json", result.Content.Headers.ContentType.MediaType);
}

[Theory(DisplayName = "Servers MUST return content type 'application/vnd.api+json'")]
[InlineData(Paths.SingleResource)]
[InlineData(Paths.ResourceCollection)]
Expand Down