Skip to content

Commit 1de9559

Browse files
authored
Merge pull request #46 from Research-Institute/staging
v1.0.0
2 parents 3c74bec + e09ee68 commit 1de9559

File tree

64 files changed

+1853
-153
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+1853
-153
lines changed

.travis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ dotnet: 1.0.0-preview2-1-003177
1010
branches:
1111
only:
1212
- master
13+
- staging
1314
script:
14-
- ./build.sh
15+
- ./build.sh

README.md

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,24 @@ Your models should inherit `Identifiable<TId>` where `TId` is the type of the pr
7676

7777
```csharp
7878
public class Person : Identifiable<Guid>
79-
{
80-
public override Guid Id { get; set; }
79+
{ }
80+
```
81+
82+
You can use the non-generic `Identifiable` if your primary key is an integer:
83+
84+
```csharp
85+
public class Person : Identifiable
86+
{ }
87+
```
88+
89+
If you need to hang annotations or attributes on the `Id` property, you can override the virtual member:
90+
91+
```csharp
92+
public class Person : Identifiable
93+
{
94+
[Key]
95+
[Column("person_id")]
96+
public override int Id { get; set; }
8197
}
8298
```
8399

@@ -89,8 +105,6 @@ add the `AttrAttribute` and provide the outbound name.
89105
```csharp
90106
public class Person : Identifiable<int>
91107
{
92-
public override int Id { get; set; }
93-
94108
[Attr("first-name")]
95109
public string FirstName { get; set; }
96110
}
@@ -99,16 +113,15 @@ public class Person : Identifiable<int>
99113
#### Relationships
100114

101115
In order for navigation properties to be identified in the model,
102-
they should be labeled as virtual.
116+
they should be labeled with the appropriate attribute (either `HasOne` or `HasMany`).
103117

104118
```csharp
105119
public class Person : Identifiable<int>
106120
{
107-
public override int Id { get; set; }
108-
109121
[Attr("first-name")]
110122
public string FirstName { get; set; }
111123

124+
[HasMany("todo-items")]
112125
public virtual List<TodoItem> TodoItems { get; set; }
113126
}
114127
```
@@ -119,12 +132,12 @@ For example, a `TodoItem` may have an `Owner` and so the Id attribute should be
119132
```csharp
120133
public class TodoItem : Identifiable<int>
121134
{
122-
public override int Id { get; set; }
123-
124135
[Attr("description")]
125136
public string Description { get; set; }
126137

127138
public int OwnerId { get; set; }
139+
140+
[HasOne("owner")]
128141
public virtual Person Owner { get; set; }
129142
}
130143
```
@@ -224,6 +237,9 @@ public class MyAuthorizedEntityRepository : DefaultEntityRepository<MyEntity>
224237
}
225238
```
226239

240+
For more examples, take a look at the customization tests
241+
in `./test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility`.
242+
227243
### Pagination
228244

229245
Resources can be paginated.
@@ -272,6 +288,7 @@ identifier):
272288
?filter[attribute]=gt:value
273289
?filter[attribute]=le:value
274290
?filter[attribute]=ge:value
291+
?filter[attribute]=like:value
275292
```
276293

277294
### Sorting

appveyor.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pull_requests:
44
branches:
55
only:
66
- master
7+
- staging
78
nuget:
89
disable_publish_on_pr: true
910
build_script:
@@ -19,7 +20,7 @@ deploy:
1920
secure: 6CeYcZ4Ze+57gxfeuHzqP6ldbUkPtF6pfpVM1Gw/K2jExFrAz763gNAQ++tiacq3
2021
skip_symbols: true
2122
on:
22-
branch: master
23+
branch: staging
2324
- provider: NuGet
2425
name: production
2526
api_key:

src/JsonApiDotNetCore/Builders/DocumentBuilder.cs

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ public Document Build(IIdentifiable entity)
2626
var document = new Document
2727
{
2828
Data = _getData(contextEntity, entity),
29-
Meta = _getMeta(entity)
29+
Meta = _getMeta(entity),
30+
Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext))
3031
};
3132

3233
document.Included = _appendIncludedObject(document.Included, contextEntity, entity);
@@ -45,7 +46,8 @@ public Documents Build(IEnumerable<IIdentifiable> entities)
4546
var documents = new Documents
4647
{
4748
Data = new List<DocumentData>(),
48-
Meta = _getMeta(entities.FirstOrDefault())
49+
Meta = _getMeta(entities.FirstOrDefault()),
50+
Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext))
4951
};
5052

5153
foreach (var entity in entities)
@@ -68,7 +70,7 @@ private Dictionary<string, object> _getMeta(IIdentifiable entity)
6870
meta = metaEntity.GetMeta(_jsonApiContext);
6971

7072
if(_jsonApiContext.Options.IncludeTotalRecordCount)
71-
meta["total-records"] = _jsonApiContext.TotalRecords;
73+
meta["total-records"] = _jsonApiContext.PageManager.TotalRecords;
7274

7375
if(meta.Count > 0) return meta;
7476
return null;
@@ -122,23 +124,25 @@ private void _addRelationships(DocumentData data, ContextEntity contextEntity, I
122124
{
123125
Links = new Links
124126
{
125-
Self = linkBuilder.GetSelfRelationLink(contextEntity.EntityName, entity.Id.ToString(), r.RelationshipName),
126-
Related = linkBuilder.GetRelatedRelationLink(contextEntity.EntityName, entity.Id.ToString(), r.RelationshipName)
127+
Self = linkBuilder.GetSelfRelationLink(contextEntity.EntityName, entity.Id.ToString(), r.InternalRelationshipName),
128+
Related = linkBuilder.GetRelatedRelationLink(contextEntity.EntityName, entity.Id.ToString(), r.InternalRelationshipName)
127129
}
128130
};
129131

130-
if (_relationshipIsIncluded(r.RelationshipName))
132+
if (_relationshipIsIncluded(r.InternalRelationshipName))
131133
{
132134
var navigationEntity = _jsonApiContext.ContextGraph
133-
.GetRelationship(entity, r.RelationshipName);
135+
.GetRelationship(entity, r.InternalRelationshipName);
134136

135-
if (navigationEntity is IEnumerable)
136-
relationshipData.ManyData = _getRelationships((IEnumerable<object>)navigationEntity, r.RelationshipName);
137+
if(navigationEntity == null)
138+
relationshipData.SingleData = null;
139+
else if (navigationEntity is IEnumerable)
140+
relationshipData.ManyData = _getRelationships((IEnumerable<object>)navigationEntity, r.InternalRelationshipName);
137141
else
138-
relationshipData.SingleData = _getRelationship(navigationEntity, r.RelationshipName);
142+
relationshipData.SingleData = _getRelationship(navigationEntity, r.InternalRelationshipName);
139143
}
140144

141-
data.Relationships.Add(r.RelationshipName.Dasherize(), relationshipData);
145+
data.Relationships.Add(r.InternalRelationshipName.Dasherize(), relationshipData);
142146
});
143147
}
144148

@@ -148,9 +152,9 @@ private List<DocumentData> _getIncludedEntities(ContextEntity contextEntity, IId
148152

149153
contextEntity.Relationships.ForEach(r =>
150154
{
151-
if (!_relationshipIsIncluded(r.RelationshipName)) return;
155+
if (!_relationshipIsIncluded(r.InternalRelationshipName)) return;
152156

153-
var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(entity, r.RelationshipName);
157+
var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(entity, r.InternalRelationshipName);
154158

155159
if (navigationEntity is IEnumerable)
156160
foreach (var includedEntity in (IEnumerable)navigationEntity)
@@ -164,6 +168,8 @@ private List<DocumentData> _getIncludedEntities(ContextEntity contextEntity, IId
164168

165169
private DocumentData _getIncludedEntity(IIdentifiable entity)
166170
{
171+
if(entity == null) return null;
172+
167173
var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(entity.GetType());
168174

169175
var data = new DocumentData

src/JsonApiDotNetCore/Builders/LinkBuilder.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
using JsonApiDotNetCore.Extensions;
32
using JsonApiDotNetCore.Services;
43
using Microsoft.AspNetCore.Http;
@@ -45,5 +44,10 @@ public string GetRelatedRelationLink(string parent, string parentId, string chil
4544
{
4645
return $"{_context.BasePath}/{parent.Dasherize()}/{parentId}/{child.Dasherize()}";
4746
}
47+
48+
public string GetPageLink(int pageOffset, int pageSize)
49+
{
50+
return $"{_context.BasePath}/{_context.RequestEntity.EntityName.Dasherize()}?page[size]={pageSize}&page[number]={pageOffset}";
51+
}
4852
}
4953
}

src/JsonApiDotNetCore/Controllers/JsonApiController.cs

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public virtual async Task<IActionResult> GetAsync()
6565
entities = IncludeRelationships(entities, _jsonApiContext.QuerySet.IncludedRelationships);
6666

6767
if (_jsonApiContext.Options.IncludeTotalRecordCount)
68-
_jsonApiContext.TotalRecords = await entities.CountAsync();
68+
_jsonApiContext.PageManager.TotalRecords = await entities.CountAsync();
6969

7070
// pagination should be done last since it will execute the query
7171
var pagedEntities = await ApplyPageQueryAsync(entities);
@@ -126,9 +126,6 @@ public virtual async Task<IActionResult> GetRelationshipAsync(TId id, string rel
126126
var relationship = _jsonApiContext.ContextGraph
127127
.GetRelationship<T>(entity, relationshipName);
128128

129-
if (relationship == null)
130-
return NotFound();
131-
132129
return Ok(relationship);
133130
}
134131

@@ -141,9 +138,13 @@ public virtual async Task<IActionResult> PostAsync([FromBody] T entity)
141138
return UnprocessableEntity();
142139
}
143140

141+
var stringId = entity.Id.ToString();
142+
if(stringId.Length > 0 && stringId != "0")
143+
return Forbidden();
144+
144145
await _entities.CreateAsync(entity);
145146

146-
return Created(HttpContext.Request.Path, entity);
147+
return Created($"{HttpContext.Request.Path}/{entity.Id}", entity);
147148
}
148149

149150
[HttpPatch("{id}")]
@@ -157,9 +158,41 @@ public virtual async Task<IActionResult> PatchAsync(TId id, [FromBody] T entity)
157158

158159
var updatedEntity = await _entities.UpdateAsync(id, entity);
159160

161+
if(updatedEntity == null) return NotFound();
162+
160163
return Ok(updatedEntity);
161164
}
162165

166+
[HttpPatch("{id}/relationships/{relationshipName}")]
167+
public virtual async Task<IActionResult> PatchRelationshipsAsync(TId id, string relationshipName, [FromBody] List<DocumentData> relationships)
168+
{
169+
relationshipName = _jsonApiContext.ContextGraph
170+
.GetRelationshipName<T>(relationshipName.ToProperCase());
171+
172+
if (relationshipName == null)
173+
{
174+
_logger?.LogInformation($"Relationship name not specified returning 422");
175+
return UnprocessableEntity();
176+
}
177+
178+
var entity = await _entities.GetAndIncludeAsync(id, relationshipName);
179+
180+
if (entity == null)
181+
return NotFound();
182+
183+
var relationship = _jsonApiContext.ContextGraph
184+
.GetContextEntity(typeof(T))
185+
.Relationships
186+
.FirstOrDefault(r => r.InternalRelationshipName == relationshipName);
187+
188+
var relationshipIds = relationships.Select(r=>r.Id);
189+
190+
await _entities.UpdateRelationshipsAsync(entity, relationship, relationshipIds);
191+
192+
return Ok();
193+
194+
}
195+
163196
[HttpDelete("{id}")]
164197
public virtual async Task<IActionResult> DeleteAsync(TId id)
165198
{
@@ -190,17 +223,15 @@ private IQueryable<T> ApplySortAndFilterQuery(IQueryable<T> entities)
190223

191224
private async Task<IEnumerable<T>> ApplyPageQueryAsync(IQueryable<T> entities)
192225
{
193-
if(_jsonApiContext.Options.DefaultPageSize == 0 && (_jsonApiContext.QuerySet == null || _jsonApiContext.QuerySet.PageQuery.PageSize == 0))
226+
var pageManager = _jsonApiContext.PageManager;
227+
if(!pageManager.IsPaginated)
194228
return entities;
195229

196230
var query = _jsonApiContext.QuerySet?.PageQuery ?? new PageQuery();
197-
198-
var pageNumber = query.PageOffset > 0 ? query.PageOffset : 1;
199-
var pageSize = query.PageSize > 0 ? query.PageSize : _jsonApiContext.Options.DefaultPageSize;
200231

201-
_logger?.LogInformation($"Applying paging query. Fetching page {pageNumber} with {pageSize} entities");
232+
_logger?.LogInformation($"Applying paging query. Fetching page {pageManager.CurrentPage} with {pageManager.PageSize} entities");
202233

203-
return await _entities.PageAsync(entities, pageSize, pageNumber);
234+
return await _entities.PageAsync(entities, pageManager.PageSize, pageManager.CurrentPage);
204235
}
205236

206237
private IQueryable<T> IncludeRelationships(IQueryable<T> entities, List<string> relationships)

src/JsonApiDotNetCore/Controllers/JsonApiControllerMixin.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,10 @@ protected IActionResult UnprocessableEntity()
1111
{
1212
return new StatusCodeResult(422);
1313
}
14+
15+
protected IActionResult Forbidden()
16+
{
17+
return new StatusCodeResult(403);
18+
}
1419
}
1520
}

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,13 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)
105105

106106
await _context.SaveChangesAsync();
107107

108-
return oldEntity;
108+
return oldEntity;
109+
}
110+
111+
public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds)
112+
{
113+
var genericProcessor = GenericProcessorFactory.GetProcessor(relationship.Type, _context);
114+
await genericProcessor.UpdateRelationshipsAsync(parent, relationship, relationshipIds);
109115
}
110116

111117
public virtual async Task<bool> DeleteAsync(TId id)
@@ -125,7 +131,7 @@ public virtual async Task<bool> DeleteAsync(TId id)
125131
public virtual IQueryable<TEntity> Include(IQueryable<TEntity> entities, string relationshipName)
126132
{
127133
var entity = _jsonApiContext.RequestEntity;
128-
if(entity.Relationships.Any(r => r.RelationshipName == relationshipName))
134+
if(entity.Relationships.Any(r => r.InternalRelationshipName == relationshipName))
129135
return entities.Include(relationshipName);
130136

131137
throw new JsonApiException("400", "Invalid relationship",

src/JsonApiDotNetCore/Data/IEntityRepository.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Collections.Generic;
22
using System.Linq;
33
using System.Threading.Tasks;
4+
using JsonApiDotNetCore.Internal;
45
using JsonApiDotNetCore.Internal.Query;
56
using JsonApiDotNetCore.Models;
67

@@ -33,6 +34,8 @@ public interface IEntityRepository<TEntity, in TId>
3334

3435
Task<TEntity> UpdateAsync(TId id, TEntity entity);
3536

37+
Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable<string> relationshipIds);
38+
3639
Task<bool> DeleteAsync(TId id);
3740
}
3841
}

src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public static IQueryable<TSource> Filter<TSource>(this IQueryable<TSource> sourc
8686
// {1}
8787
var right = Expression.Constant(convertedValue, property.PropertyType);
8888

89-
var body = Expression.Equal(left, right);
89+
Expression body;
9090
switch (filterQuery.FilterOperation)
9191
{
9292
case FilterOperations.eq:
@@ -109,6 +109,12 @@ public static IQueryable<TSource> Filter<TSource>(this IQueryable<TSource> sourc
109109
// {model.Id <= 1}
110110
body = Expression.GreaterThanOrEqual(left, right);
111111
break;
112+
case FilterOperations.like:
113+
// {model.Id <= 1}
114+
body = Expression.Call(left, "Contains", null, right);
115+
break;
116+
default:
117+
throw new JsonApiException("500", $"Unknown filter operation {filterQuery.FilterOperation}");
112118
}
113119

114120
var lambda = Expression.Lambda<Func<TSource, bool>>(body, parameter);

src/JsonApiDotNetCore/Formatters/JsonApiInputFormatter.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ public Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
4141
{
4242
var body = GetRequestBody(context.HttpContext.Request.Body);
4343
var jsonApiContext = GetService<IJsonApiContext>(context);
44-
var model = JsonApiDeSerializer.Deserialize(body, jsonApiContext);
44+
var model = jsonApiContext.IsRelationshipPath ?
45+
JsonApiDeSerializer.DeserializeRelationship(body, jsonApiContext) :
46+
JsonApiDeSerializer.Deserialize(body, jsonApiContext);
4547

4648
if(model == null)
4749
logger?.LogError("An error occurred while de-serializing the payload");

0 commit comments

Comments
 (0)