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

Initial implementation to support bulk extension #218

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions Saule/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
internal static class Constants
{
public const string MediaType = "application/vnd.api+json";
public const string MediaTypeBulkExtension = "application/vnd.api+json; ext=bulk";

public static class PropertyNames
{
Expand Down
48 changes: 48 additions & 0 deletions Saule/Http/BulkExtRouteAttributeAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.Web.Http.Routing;

namespace Saule.Http
{
/// <summary>
/// Custom route attribute to support negotiating the same route depending on content type to support the bulk extension.
/// </summary>
public class BulkExtRouteAttributeAttribute : RouteFactoryAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="BulkExtRouteAttributeAttribute"/> class.
/// </summary>
/// <param name="template">Route name</param>
/// <param name="multiple">Sets whether this route is standard JSON API or bulk extension enabled</param>
public BulkExtRouteAttributeAttribute(string template, bool multiple)
: base(template)
{
Multiple = multiple;
}

/// <summary>
/// Gets overriden constraints handling to select route based on the attribute.
/// </summary>
public override IDictionary<string, object> Constraints
{
get
{
var constraints = new HttpRouteValueDictionary();
if (Multiple)
{
constraints.Add("Content-Type", new ContentTypeConstraint(Constants.MediaTypeBulkExtension));
}
else
{
constraints.Add("Content-Type", new ContentTypeConstraint(Constants.MediaType));
}

return constraints;
}
}

/// <summary>
/// Gets a value indicating whether this route is standard JSON API or bulk extension enabled
/// </summary>
public bool Multiple { get; private set; }
}
}
42 changes: 42 additions & 0 deletions Saule/Http/ContentTypeConstraint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Web.Http.Routing;

namespace Saule.Http
{
internal class ContentTypeConstraint : IHttpRouteConstraint
{
public ContentTypeConstraint(string allowedMediaType)
{
AllowedMediaType = allowedMediaType;
}

public string AllowedMediaType { get; private set; }

public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection)
{
if (routeDirection == HttpRouteDirection.UriResolution)
{
return GetMediaHeader(request) == AllowedMediaType;
}
else
{
return true;
}
}

private string GetMediaHeader(HttpRequestMessage request)
{
IEnumerable<string> headerValues;
if (request.Content.Headers.TryGetValues("Content-Type", out headerValues) && headerValues.Count() == 1)
{
return headerValues.First();
}
else
{
return "application/vnd.api+json";
}
}
}
}
2 changes: 1 addition & 1 deletion Saule/Http/ReturnsResourceAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public override void OnActionExecuting(HttpActionContext actionContext)
}

var contentType = actionContext.Request.Content?.Headers?.ContentType;
if (contentType != null && contentType.Parameters.Any())
if (contentType != null && contentType.Parameters.Where(p => p.Name != "ext").Any())
{
// client is sending json api media type with parameters
actionContext.Response = new HttpResponseMessage(HttpStatusCode.UnsupportedMediaType);
Expand Down
2 changes: 2 additions & 0 deletions Saule/Saule.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,13 @@
</ItemGroup>
<ItemGroup>
<Compile Include="ApiResource.cs" />
<Compile Include="Http\ContentTypeConstraint.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="Http\BulkExtRouteAttributeAttribute.cs" />
<Compile Include="JsonApiSerializerOfT.cs" />
<Compile Include="Queries\Fieldset\FieldsetContext.cs" />
<Compile Include="Queries\Fieldset\FieldsetProperty.cs" />
Expand Down
20 changes: 18 additions & 2 deletions Tests/Controllers/PeopleController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using Saule.Http;
Expand Down Expand Up @@ -123,7 +124,6 @@ public IEnumerable<Person> ManualTypedQueryAndPaginatePeople([FromUri] PersonFil
return data;
}


[HttpPost]
[Route("people/{id}")]
public Person PostPerson(string id, Person person)
Expand All @@ -132,6 +132,22 @@ public Person PostPerson(string id, Person person)
return person;
}

[HttpPost]
[BulkExtRouteAttribute("people", multiple: false)]
public Person PostNewPerson(Person person)
{
person.Identifier = Guid.NewGuid().ToString();
return person;
}

[HttpPost]
[BulkExtRouteAttribute("people", multiple: true)]
public IEnumerable<Person> PostNewPeople(IEnumerable<Person> people)
{
var newPeople = people.Select(p => { p.Identifier = Guid.NewGuid().ToString(); return p; } );
return newPeople;
}

[HttpGet]
[Route("people")]
public IEnumerable<Person> GetPeople()
Expand Down
34 changes: 34 additions & 0 deletions Tests/Integration/ContentNegotiationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class ObsoleteSetup : IClassFixture<ObsoleteSetupJsonApiServer>
private readonly ObsoleteSetupJsonApiServer _server;

private readonly string _personContent = Properties.Resources.PersonResourceString;
private readonly string _peopleContent = Properties.Resources.PeopleResourceString;

public ObsoleteSetup(ObsoleteSetupJsonApiServer server)
{
Expand Down Expand Up @@ -53,6 +54,39 @@ public async Task MustReturnJsonApiContentType(string path)
Assert.Equal("application/vnd.api+json", result.Content.Headers.ContentType.MediaType);
}

[Theory(DisplayName = "Servers MUST respond with '200 OK' to valid POST content")]
[InlineData(Paths.SingleResource)]
public async Task MustReturn200OKOnCreate(string path)
{
var target = _server.GetClient();
var mediaType = new MediaTypeHeaderValue(Constants.MediaType);

HttpContent content = new StringContent(_personContent);
content.Headers.ContentType = mediaType;

var result = await target.PostAsync(path, content);

Assert.Equal(HttpStatusCode.OK, result.StatusCode);
}

[Theory(DisplayName = "Servers MUST respond with '200 OK' to valid POST content for bulk extension")]
[InlineData(Paths.ResourceCollection)]
public async Task MustReturn200OKOnCreateBulk(string path)
{
var target = _server.GetClient();

var mediaType = new MediaTypeHeaderValue(Constants.MediaType);
mediaType.Parameters.Add(new NameValueHeaderValue("ext", "bulk"));
HttpContent content = new StringContent(_peopleContent);
content.Headers.ContentType = mediaType;

var result = await target.PostAsync(path, content);

var resultContent = await result.Content.ReadAsStringAsync();

Assert.Equal(HttpStatusCode.OK, result.StatusCode);
}

[Theory(DisplayName = "Servers MUST respond with '415 Not supported' to media type parameters in content-type header")]
[InlineData("version", "1")]
[InlineData("charset", "utf-8")]
Expand Down
27 changes: 27 additions & 0 deletions Tests/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions Tests/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,27 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="PeopleResourceString" xml:space="preserve">
<value>{
"data": [{
"type": "person",
"attributes": {
"first-name": "John",
"last-name": "Smith",
"age": 34,
"number-of-legs": 2
}
}, {
"type": "person",
"attributes": {
"first-name": "Smith",
"last-name": "John",
"age": 33,
"number-of-legs": 2
}
}]
}</value>
</data>
<data name="PersonResourceString" xml:space="preserve">
<value>{
"data": {
Expand Down