Skip to content

Commit 3d51f1c

Browse files
authored
Merge pull request #58 from Research-Institute/feature/client-generated-ids
Feature/client generated ids
2 parents 6cbc1cd + 96bcc18 commit 3d51f1c

File tree

5 files changed

+170
-10
lines changed

5 files changed

+170
-10
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ JsonApiDotnetCore provides a framework for building [json:api](http://jsonapi.or
2626
- [Filtering](#filtering)
2727
- [Sorting](#sorting)
2828
- [Meta](#meta)
29+
- [Client Generated Ids](#client-generated-ids)
2930
- [Tests](#tests)
3031

3132
## Comprehensive Demo
@@ -342,6 +343,20 @@ public class Person : Identifiable<int>, IHasMeta
342343
}
343344
```
344345

346+
### Client Generated Ids
347+
348+
By default, the server will respond with a `403 Forbidden` HTTP Status Code if a `POST` request is
349+
received with a client generated id. However, this can be allowed by setting the `AllowClientGeneratedIds`
350+
flag in the options:
351+
352+
```csharp
353+
services.AddJsonApi<AppDbContext>(opt =>
354+
{
355+
opt.AllowClientGeneratedIds = true;
356+
// ..
357+
});
358+
```
359+
345360
## Tests
346361

347362
I am using DotNetCoreDocs to generate sample requests and documentation.

src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ public class JsonApiOptions
55
public string Namespace { get; set; }
66
public int DefaultPageSize { get; set; }
77
public bool IncludeTotalRecordCount { get; set; }
8+
public bool AllowClientGeneratedIds { get; set; }
89
}
910
}

src/JsonApiDotNetCore/Controllers/JsonApiController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ public virtual async Task<IActionResult> PostAsync([FromBody] T entity)
138138
return UnprocessableEntity();
139139
}
140140

141-
if (!string.IsNullOrEmpty(entity.StringId))
141+
if (!_jsonApiContext.Options.AllowClientGeneratedIds && !string.IsNullOrEmpty(entity.StringId))
142142
return Forbidden();
143143

144144
await _entities.CreateAsync(entity);

test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/CreatingDataTests.cs

Lines changed: 107 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
using System.Collections.Generic;
1818
using System.Linq;
1919
using Microsoft.EntityFrameworkCore;
20+
using JsonApiDotNetCoreExampleTests.Startups;
21+
using System;
2022

2123
namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec
2224
{
@@ -37,7 +39,7 @@ public CreatingDataTests(DocsFixture<Startup, JsonDocWriter> fixture)
3739
}
3840

3941
[Fact]
40-
public async Task Can_Create_Guid_Identifiable_Entities()
42+
public async Task Can_Create_Guid_Identifiable_Entity()
4143
{
4244
// arrange
4345
var builder = new WebHostBuilder()
@@ -74,7 +76,7 @@ public async Task Can_Create_Guid_Identifiable_Entities()
7476
};
7577
request.Content = new StringContent(JsonConvert.SerializeObject(content));
7678
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json");
77-
79+
7880
// act
7981
var response = await client.SendAsync(request);
8082

@@ -83,9 +85,10 @@ public async Task Can_Create_Guid_Identifiable_Entities()
8385
}
8486

8587
[Fact]
86-
public async Task Request_With_ClientGeneratedId_Returns_403()
88+
public async Task Cannot_Create_Entity_With_Client_Generate_Id()
8789
{
8890
// arrange
91+
var context = _fixture.GetService<AppDbContext>();
8992
var builder = new WebHostBuilder()
9093
.UseStartup<Startup>();
9194
var httpMethod = new HttpMethod("POST");
@@ -94,29 +97,124 @@ public async Task Request_With_ClientGeneratedId_Returns_403()
9497
var client = server.CreateClient();
9598
var request = new HttpRequestMessage(httpMethod, route);
9699
var todoItem = _todoItemFaker.Generate();
100+
const int clientDefinedId = 9999;
97101
var content = new
98102
{
99103
data = new
100104
{
101105
type = "todo-items",
102-
id = "9999",
106+
id = $"{clientDefinedId}",
103107
attributes = new
104108
{
105109
description = todoItem.Description,
106110
ordinal = todoItem.Ordinal
107111
}
108112
}
109113
};
114+
110115
request.Content = new StringContent(JsonConvert.SerializeObject(content));
111116
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json");
112-
117+
113118
// act
114119
var response = await client.SendAsync(request);
115120

116121
// assert
117122
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
118123
}
119124

125+
[Fact]
126+
public async Task Can_Create_Entity_With_Client_Defined_Id_If_Configured()
127+
{
128+
// arrange
129+
var context = _fixture.GetService<AppDbContext>();
130+
var builder = new WebHostBuilder()
131+
.UseStartup<ClientGeneratedIdsStartup>();
132+
var httpMethod = new HttpMethod("POST");
133+
var route = "/api/v1/todo-items";
134+
var server = new TestServer(builder);
135+
var client = server.CreateClient();
136+
var request = new HttpRequestMessage(httpMethod, route);
137+
var todoItem = _todoItemFaker.Generate();
138+
const int clientDefinedId = 9999;
139+
var content = new
140+
{
141+
data = new
142+
{
143+
type = "todo-items",
144+
id = $"{clientDefinedId}",
145+
attributes = new
146+
{
147+
description = todoItem.Description,
148+
ordinal = todoItem.Ordinal
149+
}
150+
}
151+
};
152+
153+
request.Content = new StringContent(JsonConvert.SerializeObject(content));
154+
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json");
155+
156+
// act
157+
var response = await client.SendAsync(request);
158+
var body = await response.Content.ReadAsStringAsync();
159+
var deserializedBody = (TodoItem)JsonApiDeSerializer.Deserialize(body, _jsonApiContext, context);
160+
161+
// assert
162+
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
163+
Assert.Equal(clientDefinedId, deserializedBody.Id);
164+
}
165+
166+
167+
[Fact]
168+
public async Task Can_Create_Guid_Identifiable_Entity_With_Client_Defined_Id_If_Configured()
169+
{
170+
// arrange
171+
var builder = new WebHostBuilder()
172+
.UseStartup<ClientGeneratedIdsStartup>();
173+
var httpMethod = new HttpMethod("POST");
174+
var server = new TestServer(builder);
175+
var client = server.CreateClient();
176+
177+
var context = _fixture.GetService<AppDbContext>();
178+
179+
var owner = new JsonApiDotNetCoreExample.Models.Person();
180+
context.People.Add(owner);
181+
await context.SaveChangesAsync();
182+
183+
var route = "/api/v1/todo-item-collections";
184+
var request = new HttpRequestMessage(httpMethod, route);
185+
var clientDefinedId = Guid.NewGuid();
186+
var content = new
187+
{
188+
data = new
189+
{
190+
type = "todo-item-collections",
191+
id = $"{clientDefinedId}",
192+
relationships = new
193+
{
194+
owner = new
195+
{
196+
data = new
197+
{
198+
type = "people",
199+
id = owner.Id.ToString()
200+
}
201+
}
202+
}
203+
}
204+
};
205+
request.Content = new StringContent(JsonConvert.SerializeObject(content));
206+
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json");
207+
208+
// act
209+
var response = await client.SendAsync(request);
210+
var body = await response.Content.ReadAsStringAsync();
211+
var deserializedBody = (TodoItemCollection)JsonApiDeSerializer.Deserialize(body, _jsonApiContext, context);
212+
213+
// assert
214+
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
215+
Assert.Equal(clientDefinedId, deserializedBody.Id);
216+
}
217+
120218
[Fact]
121219
public async Task Can_Create_And_Set_HasMany_Relationships()
122220
{
@@ -167,14 +265,14 @@ public async Task Can_Create_And_Set_HasMany_Relationships()
167265

168266
request.Content = new StringContent(JsonConvert.SerializeObject(content));
169267
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json");
170-
268+
171269
// act
172270
var response = await client.SendAsync(request);
173271
var body = await response.Content.ReadAsStringAsync();
174272
var deserializedBody = (TodoItemCollection)JsonApiDeSerializer.Deserialize(body, _jsonApiContext, context);
175273
var newId = deserializedBody.Id;
176274
var contextCollection = context.TodoItemCollections
177-
.Include(c=> c.Owner)
275+
.Include(c => c.Owner)
178276
.Include(c => c.TodoItems)
179277
.SingleOrDefault(c => c.Id == newId);
180278

@@ -210,7 +308,7 @@ public async Task ShouldReceiveLocationHeader_InResponse()
210308
};
211309
request.Content = new StringContent(JsonConvert.SerializeObject(content));
212310
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json");
213-
311+
214312
// act
215313
var response = await client.SendAsync(request);
216314
var body = await response.Content.ReadAsStringAsync();
@@ -247,7 +345,7 @@ public async Task Respond_409_ToIncorrectEntityType()
247345
};
248346
request.Content = new StringContent(JsonConvert.SerializeObject(content));
249347
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json");
250-
348+
251349
// act
252350
var response = await client.SendAsync(request);
253351

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using Microsoft.AspNetCore.Hosting;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.Logging;
4+
using JsonApiDotNetCoreExample.Data;
5+
using Microsoft.EntityFrameworkCore;
6+
using JsonApiDotNetCore.Extensions;
7+
using DotNetCoreDocs.Configuration;
8+
using System;
9+
using JsonApiDotNetCoreExample;
10+
11+
namespace JsonApiDotNetCoreExampleTests.Startups
12+
{
13+
public class ClientGeneratedIdsStartup : Startup
14+
{
15+
public ClientGeneratedIdsStartup(IHostingEnvironment env)
16+
: base (env)
17+
{ }
18+
19+
public override IServiceProvider ConfigureServices(IServiceCollection services)
20+
{
21+
var loggerFactory = new LoggerFactory();
22+
23+
loggerFactory
24+
.AddConsole(LogLevel.Trace);
25+
26+
services.AddSingleton<ILoggerFactory>(loggerFactory);
27+
28+
services.AddDbContext<AppDbContext>(options =>
29+
{
30+
options.UseNpgsql(GetDbConnectionString());
31+
}, ServiceLifetime.Transient);
32+
33+
services.AddJsonApi<AppDbContext>(opt =>
34+
{
35+
opt.Namespace = "api/v1";
36+
opt.DefaultPageSize = 5;
37+
opt.IncludeTotalRecordCount = true;
38+
opt.AllowClientGeneratedIds = true;
39+
});
40+
41+
services.AddDocumentationConfiguration(Config);
42+
43+
return services.BuildServiceProvider();
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)