Skip to content

v1.2.0 #70

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

Merged
merged 49 commits into from
Mar 21, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
aaabb1a
feat(meta): add a meta builder class
jaredcnance Mar 16, 2017
19a3822
feat(service-provider-ext): inject IMetaBuilder
jaredcnance Mar 16, 2017
5545a91
test(service-collection-extension): test service injection
jaredcnance Mar 16, 2017
a82aade
feat(json-api-context): expose meta builder as a property of the context
jaredcnance Mar 16, 2017
c3b6af5
feat(document-builder): use the meta builder
jaredcnance Mar 16, 2017
34d56ef
feat(services): add IRequestMeta for extensibility
jaredcnance Mar 16, 2017
aa0ddfc
test(extensibility): failing test that users can define meta via serv…
jaredcnance Mar 17, 2017
340ee44
feat(document-builder): apply request meta
jaredcnance Mar 17, 2017
8462e75
docs(readme): document Request Meta
jaredcnance Mar 17, 2017
34431fa
refactor(serialization): introduce new interfaces and composition roo…
jaredcnance Mar 17, 2017
7722500
refactor(de-serialization): introduce new interfaces and composition …
jaredcnance Mar 17, 2017
5c1a148
chore(csproj): bump package version
jaredcnance Mar 17, 2017
7a95169
Merge pull request #61 from Research-Institute/feature/request-meta
jaredcnance Mar 17, 2017
588b980
test(patch): failing test for patching entity has one relationship
jaredcnance Mar 20, 2017
402d6cc
feat(relationship-attr): make setValue abstract
jaredcnance Mar 20, 2017
0f2ba28
feat(json-api-context): store a list of relationships to update
jaredcnance Mar 20, 2017
7f49ef9
feat(de-serializer): store which relationships should be updated
jaredcnance Mar 20, 2017
3143e4a
feat(entity-repository): update relationship values
jaredcnance Mar 20, 2017
f6d5c34
test(patch): use non-cached context
jaredcnance Mar 20, 2017
b322162
Merge pull request #64 from Research-Institute/fix/patch-relationships
jaredcnance Mar 20, 2017
072ced3
test(fetching-data): add failing test
jaredcnance Mar 20, 2017
ec25664
feat(document-builder): include relationship objects in compound doc
jaredcnance Mar 20, 2017
635e20f
fix(document-builder): do not include null relationships in compound doc
jaredcnance Mar 20, 2017
e2c0cc8
Merge pull request #66 from Research-Institute/fix/compound-rel-inclu…
jaredcnance Mar 20, 2017
10184ac
refactor(jsonapi-exception): treat all errors as error collection
jaredcnance Mar 20, 2017
2feec1d
feat(jsonapi-exception): add logic for getting error collectoon status
jaredcnance Mar 20, 2017
c459700
fix(error): ignore StatusCode property
jaredcnance Mar 20, 2017
75e72b7
refactor(writer): move logic into serializer
jaredcnance Mar 20, 2017
3365d59
fix(error-collection): use camel-case serialization
jaredcnance Mar 20, 2017
70b273a
test(extensibility): verify users can serialize custom errors
jaredcnance Mar 20, 2017
7fd3e48
docs(readme): document custom error usage
jaredcnance Mar 20, 2017
b8443b8
Merge pull request #68 from Research-Institute/feat/improved-errors
jaredcnance Mar 20, 2017
2b80385
docs(readme): fix syntax highlighting
jaredcnance Mar 20, 2017
99f623d
feat(IQueryable): add extension for selecting columns by list of names
jaredcnance Mar 20, 2017
1b8b6cb
fix(IQueryableExt): clean up extension
jaredcnance Mar 21, 2017
aa6faf4
test(sparse-fieldsets): validate the use of the Select extension
jaredcnance Mar 21, 2017
d50fb51
clean(error-test): remove unused ns
jaredcnance Mar 21, 2017
a2ab5bd
feat(query-set): parse fields parameter
jaredcnance Mar 21, 2017
027b3b0
fix(IQueryableExt): do not return dynamic type
jaredcnance Mar 21, 2017
66c0bd2
feat(repository): apply select query
jaredcnance Mar 21, 2017
b3c3f16
feat(document-builder): check whether or not an attribute should be inc
jaredcnance Mar 21, 2017
743edd1
test(sparse-fields): test that fields can be restricted using query
jaredcnance Mar 21, 2017
4cb6672
docs(readme): document sparse fieldset support
jaredcnance Mar 21, 2017
3d58227
chore(tests): move all helper classes into a helper dir
jaredcnance Mar 21, 2017
69afe90
feat(tests): add helper extension to get ef sql output
jaredcnance Mar 21, 2017
ce969ed
feat(tests): add helper extension to normalize strings
jaredcnance Mar 21, 2017
0179f9e
test(sparse-fields): validate the result SQL
jaredcnance Mar 21, 2017
560f19e
Merge pull request #69 from Research-Institute/feat/sparse-field-sets
jaredcnance Mar 21, 2017
408113c
Merge branch 'master' into develop
jaredcnance Mar 21, 2017
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
32 changes: 1 addition & 31 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,41 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (web)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceRoot}/src/JsonApiDotNetCoreExample/bin/Debug/netcoreapp1.0/JsonApiDotNetCoreExample.dll",
"args": [],
"cwd": "${workspaceRoot}/src/JsonApiDotNetCoreExample",
"stopAtEntry": false,
"launchBrowser": {
"enabled": false,
"args": "${auto-detect-url}",
"windows": {
"command": "cmd.exe",
"args": "/C start ${auto-detect-url}"
},
"osx": {
"command": "open"
},
"linux": {
"command": "xdg-open"
}
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceRoot}/Views"
}
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command.pickProcess}"
"processId": "${command:pickProcess}"
}
]
}
62 changes: 60 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ JsonApiDotnetCore provides a framework for building [json:api](http://jsonapi.or
- [Sorting](#sorting)
- [Meta](#meta)
- [Client Generated Ids](#client-generated-ids)
- [Custom Errors](#custom-errors)
- [Sparse Fieldsets](#sparse-fieldsets)
- [Tests](#tests)

## Comprehensive Demo
Expand All @@ -44,14 +46,14 @@ Install-Package JsonApiDotnetCore

- project.json
```json
"JsonApiDotNetCore": "1.1.0"
"JsonApiDotNetCore": "1.2.0"
```

- *.csproj
```xml
<ItemGroup>
<!-- ... -->
<PackageReference Include="JsonApiDotNetCore" Version="1.1.0" />
<PackageReference Include="JsonApiDotNetCore" Version="1.2.0" />
</ItemGroup>
```

Expand Down Expand Up @@ -326,6 +328,10 @@ Resources can be sorted by an attribute:

### Meta

Meta objects can be assigned in two ways:
- Resource meta
- Request Meta

Resource meta can be defined by implementing `IHasMeta` on the model class:

```csharp
Expand All @@ -343,6 +349,9 @@ public class Person : Identifiable<int>, IHasMeta
}
```

Request Meta can be added by injecting a service that implements `IRequestMeta`.
In the event of a key collision, the Request Meta will take precendence.

### Client Generated Ids

By default, the server will respond with a `403 Forbidden` HTTP Status Code if a `POST` request is
Expand All @@ -357,6 +366,55 @@ services.AddJsonApi<AppDbContext>(opt =>
});
```

### Custom Errors

By default, errors will only contain the properties defined by the internal [Error](https://github.com/Research-Institute/json-api-dotnet-core/blob/master/src/JsonApiDotNetCore/Internal/Error.cs) class. However, you can create your own by inheriting from `Error` and either throwing it in a `JsonApiException` or returning the error from your controller.

```csharp
// custom error definition
public class CustomError : Error {
public CustomError(string status, string title, string detail, string myProp)
: base(status, title, detail)
{
MyCustomProperty = myProp;
}
public string MyCustomProperty { get; set; }
}

// throwing a custom error
public void MyMethod() {
var error = new CustomError("507", "title", "detail", "custom");
throw new JsonApiException(error);
}

// returning from controller
[HttpPost]
public override async Task<IActionResult> PostAsync([FromBody] MyEntity entity)
{
if(_db.IsFull)
return new ObjectResult(new CustomError("507", "Database is full.", "Theres no more room.", "Sorry."));

// ...
}
```

### Sparse Fieldsets

We currently support top-level field selection.
What this means is you can restrict which fields are returned by a query using the `fields` query parameter, but this does not yet apply to included relationships.

- Currently valid:
```http
GET /articles?fields[articles]=title,body HTTP/1.1
Accept: application/vnd.api+json
```

- Not yet supported:
```http
GET /articles?include=author&fields[articles]=title,body&fields[people]=name HTTP/1.1
Accept: application/vnd.api+json
```

## Tests

I am using DotNetCoreDocs to generate sample requests and documentation.
Expand Down
94 changes: 58 additions & 36 deletions src/JsonApiDotNetCore/Builders/DocumentBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,37 @@

namespace JsonApiDotNetCore.Builders
{
public class DocumentBuilder
public class DocumentBuilder : IDocumentBuilder
{
private IJsonApiContext _jsonApiContext;
private IContextGraph _contextGraph;
private readonly IRequestMeta _requestMeta;

public DocumentBuilder(IJsonApiContext jsonApiContext)
{
_jsonApiContext = jsonApiContext;
_contextGraph = jsonApiContext.ContextGraph;
}

public DocumentBuilder(IJsonApiContext jsonApiContext, IRequestMeta requestMeta)
{
_jsonApiContext = jsonApiContext;
_contextGraph = jsonApiContext.ContextGraph;
_requestMeta = requestMeta;
}

public Document Build(IIdentifiable entity)
{
var contextEntity = _contextGraph.GetContextEntity(entity.GetType());

var document = new Document
{
Data = _getData(contextEntity, entity),
Meta = _getMeta(entity),
Data = GetData(contextEntity, entity),
Meta = GetMeta(entity),
Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext))
};

document.Included = _appendIncludedObject(document.Included, contextEntity, entity);
document.Included = AppendIncludedObject(document.Included, contextEntity, entity);

return document;
}
Expand All @@ -46,39 +54,42 @@ public Documents Build(IEnumerable<IIdentifiable> entities)
var documents = new Documents
{
Data = new List<DocumentData>(),
Meta = _getMeta(entities.FirstOrDefault()),
Meta = GetMeta(entities.FirstOrDefault()),
Links = _jsonApiContext.PageManager.GetPageLinks(new LinkBuilder(_jsonApiContext))
};

foreach (var entity in entities)
{
documents.Data.Add(_getData(contextEntity, entity));
documents.Included = _appendIncludedObject(documents.Included, contextEntity, entity);
documents.Data.Add(GetData(contextEntity, entity));
documents.Included = AppendIncludedObject(documents.Included, contextEntity, entity);
}

return documents;
}

private Dictionary<string, object> _getMeta(IIdentifiable entity)
private Dictionary<string, object> GetMeta(IIdentifiable entity)
{
if (entity == null) return null;

var meta = new Dictionary<string, object>();
var metaEntity = entity as IHasMeta;

if(metaEntity != null)
meta = metaEntity.GetMeta(_jsonApiContext);
var builder = _jsonApiContext.MetaBuilder;

if(entity is IHasMeta metaEntity)
builder.Add(metaEntity.GetMeta(_jsonApiContext));

if(_jsonApiContext.Options.IncludeTotalRecordCount)
meta["total-records"] = _jsonApiContext.PageManager.TotalRecords;
builder.Add("total-records", _jsonApiContext.PageManager.TotalRecords);

if(_requestMeta != null)
builder.Add(_requestMeta.GetMeta());

var meta = builder.Build();
if(meta.Count > 0) return meta;
return null;
}

private List<DocumentData> _appendIncludedObject(List<DocumentData> includedObject, ContextEntity contextEntity, IIdentifiable entity)
private List<DocumentData> AppendIncludedObject(List<DocumentData> includedObject, ContextEntity contextEntity, IIdentifiable entity)
{
var includedEntities = _getIncludedEntities(contextEntity, entity);
var includedEntities = GetIncludedEntities(contextEntity, entity);
if (includedEntities.Count > 0)
{
if (includedObject == null)
Expand All @@ -89,7 +100,7 @@ private List<DocumentData> _appendIncludedObject(List<DocumentData> includedObje
return includedObject;
}

private DocumentData _getData(ContextEntity contextEntity, IIdentifiable entity)
private DocumentData GetData(ContextEntity contextEntity, IIdentifiable entity)
{
var data = new DocumentData
{
Expand All @@ -104,16 +115,24 @@ private DocumentData _getData(ContextEntity contextEntity, IIdentifiable entity)

contextEntity.Attributes.ForEach(attr =>
{
data.Attributes.Add(attr.PublicAttributeName, attr.GetValue(entity));
if(ShouldIncludeAttribute(attr))
data.Attributes.Add(attr.PublicAttributeName, attr.GetValue(entity));
});

if (contextEntity.Relationships.Count > 0)
_addRelationships(data, contextEntity, entity);
AddRelationships(data, contextEntity, entity);

return data;
}

private void _addRelationships(DocumentData data, ContextEntity contextEntity, IIdentifiable entity)
private bool ShouldIncludeAttribute(AttrAttribute attr)
{
return (_jsonApiContext.QuerySet == null
|| _jsonApiContext.QuerySet.Fields.Count == 0
|| _jsonApiContext.QuerySet.Fields.Contains(attr.InternalAttributeName));
}

private void AddRelationships(DocumentData data, ContextEntity contextEntity, IIdentifiable entity)
{
data.Relationships = new Dictionary<string, RelationshipData>();
var linkBuilder = new LinkBuilder(_jsonApiContext);
Expand All @@ -129,54 +148,57 @@ private void _addRelationships(DocumentData data, ContextEntity contextEntity, I
}
};

if (_relationshipIsIncluded(r.InternalRelationshipName))
if (RelationshipIsIncluded(r.InternalRelationshipName))
{
var navigationEntity = _jsonApiContext.ContextGraph
.GetRelationship(entity, r.InternalRelationshipName);

if(navigationEntity == null)
relationshipData.SingleData = null;
else if (navigationEntity is IEnumerable)
relationshipData.ManyData = _getRelationships((IEnumerable<object>)navigationEntity, r.InternalRelationshipName);
relationshipData.ManyData = GetRelationships((IEnumerable<object>)navigationEntity, r.InternalRelationshipName);
else
relationshipData.SingleData = _getRelationship(navigationEntity, r.InternalRelationshipName);
relationshipData.SingleData = GetRelationship(navigationEntity, r.InternalRelationshipName);
}

data.Relationships.Add(r.InternalRelationshipName.Dasherize(), relationshipData);
});
}

private List<DocumentData> _getIncludedEntities(ContextEntity contextEntity, IIdentifiable entity)
private List<DocumentData> GetIncludedEntities(ContextEntity contextEntity, IIdentifiable entity)
{
var included = new List<DocumentData>();

contextEntity.Relationships.ForEach(r =>
{
if (!_relationshipIsIncluded(r.InternalRelationshipName)) return;
if (!RelationshipIsIncluded(r.InternalRelationshipName)) return;

var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(entity, r.InternalRelationshipName);

if (navigationEntity is IEnumerable)
foreach (var includedEntity in (IEnumerable)navigationEntity)
included.Add(_getIncludedEntity((IIdentifiable)includedEntity));
AddIncludedEntity(included, (IIdentifiable)includedEntity);
else
included.Add(_getIncludedEntity((IIdentifiable)navigationEntity));
AddIncludedEntity(included, (IIdentifiable)navigationEntity);
});

return included;
}

private DocumentData _getIncludedEntity(IIdentifiable entity)
private void AddIncludedEntity(List<DocumentData> entities, IIdentifiable entity)
{
var includedEntity = GetIncludedEntity(entity);
if(includedEntity != null)
entities.Add(includedEntity);
}

private DocumentData GetIncludedEntity(IIdentifiable entity)
{
if(entity == null) return null;

var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(entity.GetType());

var data = new DocumentData
{
Type = contextEntity.EntityName,
Id = entity.StringId
};
var data = GetData(contextEntity, entity);

data.Attributes = new Dictionary<string, object>();

Expand All @@ -188,13 +210,13 @@ private DocumentData _getIncludedEntity(IIdentifiable entity)
return data;
}

private bool _relationshipIsIncluded(string relationshipName)
private bool RelationshipIsIncluded(string relationshipName)
{
return _jsonApiContext.IncludedRelationships != null &&
_jsonApiContext.IncludedRelationships.Contains(relationshipName.ToProperCase());
}

private List<Dictionary<string, string>> _getRelationships(IEnumerable<object> entities, string relationshipName)
private List<Dictionary<string, string>> GetRelationships(IEnumerable<object> entities, string relationshipName)
{
var objType = entities.GetType().GenericTypeArguments[0];

Expand All @@ -210,7 +232,7 @@ private List<Dictionary<string, string>> _getRelationships(IEnumerable<object> e
}
return relationships;
}
private Dictionary<string, string> _getRelationship(object entity, string relationshipName)
private Dictionary<string, string> GetRelationship(object entity, string relationshipName)
{
var objType = entity.GetType();

Expand Down
11 changes: 11 additions & 0 deletions src/JsonApiDotNetCore/Builders/IDocumentBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Collections.Generic;
using JsonApiDotNetCore.Models;

namespace JsonApiDotNetCore.Builders
{
public interface IDocumentBuilder
{
Document Build(IIdentifiable entity);
Documents Build(IEnumerable<IIdentifiable> entities);
}
}
Loading