Skip to content

Feat/#297 #323

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

Closed
wants to merge 18 commits into from
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using JsonApiDotNetCore.Controllers;
using JsonApiDotNetCore.Services;
using JsonApiDotNetCoreExample.Models;
using Microsoft.Extensions.Logging;

namespace JsonApiDotNetCoreExample.Controllers
{
public class UsersController : JsonApiController<User>
{
public UsersController(
IJsonApiContext jsonApiContext,
IResourceService<User> resourceService,
ILoggerFactory loggerFactory)
: base(jsonApiContext, resourceService, loggerFactory)
{ }
}
}
2 changes: 1 addition & 1 deletion src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)

public DbSet<Article> Articles { get; set; }
public DbSet<Author> Authors { get; set; }

public DbSet<NonJsonApiResource> NonJsonApiResources { get; set; }
public DbSet<User> Users { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;

namespace JsonApiDotNetCoreExample.Migrations
Expand Down
10 changes: 10 additions & 0 deletions src/Examples/JsonApiDotNetCoreExample/Models/User.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using JsonApiDotNetCore.Models;

namespace JsonApiDotNetCoreExample.Models
{
public class User : Identifiable
{
[Attr("username")] public string Username { get; set; }
[Attr("password")] public string Password { get; set; }
}
}
12 changes: 12 additions & 0 deletions src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Collections.Generic;
using JsonApiDotNetCore.Models;
using JsonApiDotNetCoreExample.Models;

namespace JsonApiDotNetCoreExample.Resources
{
public class UserResource : ResourceDefinition<User>
{
protected override List<AttrAttribute> OutputAttrs()
=> Remove(user => user.Password);
}
}
7 changes: 6 additions & 1 deletion src/Examples/JsonApiDotNetCoreExample/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
using Microsoft.EntityFrameworkCore;
using JsonApiDotNetCore.Extensions;
using System;
using JsonApiDotNetCore.Models;
using JsonApiDotNetCoreExample.Resources;
using JsonApiDotNetCoreExample.Models;

namespace JsonApiDotNetCoreExample
{
Expand Down Expand Up @@ -38,7 +41,9 @@ public virtual IServiceProvider ConfigureServices(IServiceCollection services)
options.Namespace = "api/v1";
options.DefaultPageSize = 5;
options.IncludeTotalRecordCount = true;
});
})
// TODO: this should be handled via auto-discovery
.AddScoped<ResourceDefinition<User>, UserResource>();

var provider = services.BuildServiceProvider();
var appContext = provider.GetRequiredService<AppDbContext>();
Expand Down
5 changes: 1 addition & 4 deletions src/Examples/ReportsExample/Controllers/ReportsController.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using JsonApiDotNetCore.Controllers;
using JsonApiDotNetCore.Services;
Expand Down
1 change: 0 additions & 1 deletion src/Examples/ReportsExample/Services/ReportService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using JsonApiDotNetCore.Services;
Expand Down
12 changes: 9 additions & 3 deletions src/JsonApiDotNetCore/Builders/ContextGraphBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ public IContextGraph Build()
_entities.ForEach(e => e.Links = GetLinkFlags(e.EntityType));

var graph = new ContextGraph(_entities, _usesDbContext, _validationResults);

return graph;
}

Expand All @@ -83,7 +82,8 @@ public IContextGraphBuilder AddResource<TResource, TId>(string pluralizedTypeNam
EntityType = entityType,
IdentityType = idType,
Attributes = GetAttributes(entityType),
Relationships = GetRelationships(entityType)
Relationships = GetRelationships(entityType),
ResourceType = GetResourceDefinitionType(entityType)
};

private Link GetLinkFlags(Type entityType)
Expand All @@ -104,8 +104,12 @@ protected virtual List<AttrAttribute> GetAttributes(Type entityType)
foreach (var prop in properties)
{
var attribute = (AttrAttribute)prop.GetCustomAttribute(typeof(AttrAttribute));
if (attribute == null) continue;
if (attribute == null)
continue;

attribute.InternalAttributeName = prop.Name;
attribute.PropertyInfo = prop;

attributes.Add(attribute);
}
return attributes;
Expand Down Expand Up @@ -136,6 +140,8 @@ protected virtual Type GetRelationshipType(RelationshipAttribute relation, Prope
return prop.PropertyType;
}

private Type GetResourceDefinitionType(Type entityType) => typeof(ResourceDefinition<>).MakeGenericType(entityType);

public IContextGraphBuilder AddDbContext<T>() where T : DbContext
{
_usesDbContext = true;
Expand Down
43 changes: 28 additions & 15 deletions src/JsonApiDotNetCore/Builders/DocumentBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
Expand All @@ -14,22 +15,29 @@ public class DocumentBuilder : IDocumentBuilder
private readonly IContextGraph _contextGraph;
private readonly IRequestMeta _requestMeta;
private readonly DocumentBuilderOptions _documentBuilderOptions;
private readonly IScopedServiceProvider _scopedServiceProvider;

public DocumentBuilder(IJsonApiContext jsonApiContext, IRequestMeta requestMeta = null, IDocumentBuilderOptionsProvider documentBuilderOptionsProvider = null)
public DocumentBuilder(
IJsonApiContext jsonApiContext,
IRequestMeta requestMeta = null,
IDocumentBuilderOptionsProvider documentBuilderOptionsProvider = null,
IScopedServiceProvider scopedServiceProvider = null)
{
_jsonApiContext = jsonApiContext;
_contextGraph = jsonApiContext.ContextGraph;
_requestMeta = requestMeta;
_documentBuilderOptions = documentBuilderOptionsProvider?.GetDocumentBuilderOptions() ?? new DocumentBuilderOptions(); ;
_documentBuilderOptions = documentBuilderOptionsProvider?.GetDocumentBuilderOptions() ?? new DocumentBuilderOptions();
_scopedServiceProvider = scopedServiceProvider;
}

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

var resourceDefinition = _scopedServiceProvider?.GetService(contextEntity.ResourceType) as IResourceDefinition;
var document = new Document
{
Data = GetData(contextEntity, entity),
Data = GetData(contextEntity, entity, resourceDefinition),
Meta = GetMeta(entity)
};

Expand All @@ -44,8 +52,8 @@ public Document Build(IIdentifiable entity)
public Documents Build(IEnumerable<IIdentifiable> entities)
{
var entityType = entities.GetElementType();

var contextEntity = _contextGraph.GetContextEntity(entityType);
var resourceDefinition = _scopedServiceProvider?.GetService(contextEntity.ResourceType) as IResourceDefinition;

var enumeratedEntities = entities as IList<IIdentifiable> ?? entities.ToList();
var documents = new Documents
Expand All @@ -59,7 +67,7 @@ public Documents Build(IEnumerable<IIdentifiable> entities)

foreach (var entity in enumeratedEntities)
{
documents.Data.Add(GetData(contextEntity, entity));
documents.Data.Add(GetData(contextEntity, entity, resourceDefinition));
documents.Included = AppendIncludedObject(documents.Included, contextEntity, entity);
}

Expand All @@ -68,21 +76,20 @@ public Documents Build(IEnumerable<IIdentifiable> entities)

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

var builder = _jsonApiContext.MetaBuilder;

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

if (_jsonApiContext.Options.IncludeTotalRecordCount)
if (_jsonApiContext.Options.IncludeTotalRecordCount && _jsonApiContext.PageManager.TotalRecords != null)
builder.Add("total-records", _jsonApiContext.PageManager.TotalRecords);

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

if (entity != null && entity is IHasMeta metaEntity)
builder.Add(metaEntity.GetMeta(_jsonApiContext));

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

return null;
}

Expand All @@ -99,7 +106,11 @@ private List<DocumentData> AppendIncludedObject(List<DocumentData> includedObjec
return includedObject;
}

[Obsolete("You should specify an IResourceDefinition implementation using the GetData/3 overload.")]
public DocumentData GetData(ContextEntity contextEntity, IIdentifiable entity)
=> GetData(contextEntity, entity, resourceDefinition: null);

public DocumentData GetData(ContextEntity contextEntity, IIdentifiable entity, IResourceDefinition resourceDefinition = null)
{
var data = new DocumentData
{
Expand All @@ -112,7 +123,8 @@ public DocumentData GetData(ContextEntity contextEntity, IIdentifiable entity)

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

contextEntity.Attributes.ForEach(attr =>
var resourceAttributes = resourceDefinition?.GetOutputAttrs(entity) ?? contextEntity.Attributes;
resourceAttributes.ForEach(attr =>
{
var attributeValue = attr.GetValue(entity);
if (ShouldIncludeAttribute(attr, attributeValue))
Expand Down Expand Up @@ -220,8 +232,9 @@ private DocumentData GetIncludedEntity(IIdentifiable entity)
if (entity == null) return null;

var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(entity.GetType());
var resourceDefinition = _scopedServiceProvider.GetService(contextEntity.ResourceType) as IResourceDefinition;

var data = GetData(contextEntity, entity);
var data = GetData(contextEntity, entity, resourceDefinition);

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

Expand Down
6 changes: 5 additions & 1 deletion src/JsonApiDotNetCore/Builders/IDocumentBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using JsonApiDotNetCore.Internal;
using JsonApiDotNetCore.Models;
Expand All @@ -8,6 +9,9 @@ public interface IDocumentBuilder
{
Document Build(IIdentifiable entity);
Documents Build(IEnumerable<IIdentifiable> entities);

[Obsolete("You should specify an IResourceDefinition implementation using the GetData/3 overload.")]
DocumentData GetData(ContextEntity contextEntity, IIdentifiable entity);
DocumentData GetData(ContextEntity contextEntity, IIdentifiable entity, IResourceDefinition resourceDefinition = null);
}
}
}
10 changes: 10 additions & 0 deletions src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,16 @@ public class JsonApiOptions
/// </remarks>
public bool EnableOperations { get; set; }

/// <summary>
/// Whether or not to validate model state.
/// </summary>
/// <example>
/// <code>
/// options.ValidateModelState = true;
/// </code>
/// </example>
public bool ValidateModelState { get; set; }

[Obsolete("JsonContract resolver can now be set on SerializerSettings.")]
public IContractResolver JsonContractResolver
{
Expand Down
9 changes: 8 additions & 1 deletion src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using JsonApiDotNetCore.Extensions;
using JsonApiDotNetCore.Internal;
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Services;
Expand Down Expand Up @@ -145,14 +146,18 @@ public virtual async Task<IActionResult> GetRelationshipAsync(TId id, string rel

public virtual async Task<IActionResult> PostAsync([FromBody] T entity)
{
if (_create == null) throw Exceptions.UnSupportedRequestMethod;
if (_create == null)
throw Exceptions.UnSupportedRequestMethod;

if (entity == null)
return UnprocessableEntity();

if (!_jsonApiContext.Options.AllowClientGeneratedIds && !string.IsNullOrEmpty(entity.StringId))
return Forbidden();

if (_jsonApiContext.Options.ValidateModelState && !ModelState.IsValid)
return BadRequest(ModelState.ConvertToErrorCollection());

entity = await _create.CreateAsync(entity);

return Created($"{HttpContext.Request.Path}/{entity.Id}", entity);
Expand All @@ -164,6 +169,8 @@ public virtual async Task<IActionResult> PatchAsync(TId id, [FromBody] T entity)

if (entity == null)
return UnprocessableEntity();
if (_jsonApiContext.Options.ValidateModelState && !ModelState.IsValid)
return BadRequest(ModelState.ConvertToErrorCollection());

var updatedEntity = await _update.UpdateAsync(id, entity);

Expand Down
1 change: 0 additions & 1 deletion src/JsonApiDotNetCore/Controllers/JsonApiCmdController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace JsonApiDotNetCore.Controllers
{
Expand Down
2 changes: 0 additions & 2 deletions src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace JsonApiDotNetCore.Controllers
{
Expand Down
18 changes: 18 additions & 0 deletions src/JsonApiDotNetCore/DependencyInjection/ServiceLocator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;
using System.Threading;

namespace JsonApiDotNetCore.DependencyInjection
{
internal class ServiceLocator
{
public static AsyncLocal<IServiceProvider> _scopedProvider = new AsyncLocal<IServiceProvider>();
public static void Initialize(IServiceProvider serviceProvider) => _scopedProvider.Value = serviceProvider;

public static object GetService(Type type)
=> _scopedProvider.Value != null
? _scopedProvider.Value.GetService(type)
: throw new InvalidOperationException(
$"Service locator has not been initialized for the current asynchronous flow. Call {nameof(Initialize)} first."
);
}
}
1 change: 0 additions & 1 deletion src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Linq;
using JsonApiDotNetCore.Internal;
using JsonApiDotNetCore.Models;
using Microsoft.EntityFrameworkCore;

Expand Down
29 changes: 29 additions & 0 deletions src/JsonApiDotNetCore/Extensions/ModelStateExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using JsonApiDotNetCore.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.EntityFrameworkCore.Internal;

namespace JsonApiDotNetCore.Extensions
{
public static class ModelStateExtensions
{
public static ErrorCollection ConvertToErrorCollection(this ModelStateDictionary modelState)
{
ErrorCollection collection = new ErrorCollection();
foreach (var entry in modelState)
{
if (entry.Value.Errors.Any() == false)
continue;

foreach (var modelError in entry.Value.Errors)
{
if (modelError.Exception is JsonApiException jex)
collection.Errors.AddRange(jex.GetError().Errors);
else
collection.Errors.Add(new Error(400, entry.Key, modelError.ErrorMessage, modelError.Exception != null ? ErrorMeta.FromException(modelError.Exception) : null));
}
}

return collection;
}
}
}
Loading