Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 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
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@
<BogusVersion>29.0.1</BogusVersion>
<MoqVersion>4.13.1</MoqVersion>
</PropertyGroup>
</Project>
</Project>
4 changes: 2 additions & 2 deletions benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Serialization;
using JsonApiDotNetCore.Serialization.Server;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;

namespace Benchmarks.Serialization
Expand Down Expand Up @@ -38,8 +39,7 @@ public JsonApiDeserializerBenchmarks()
var options = new JsonApiOptions();
IResourceGraph resourceGraph = DependencyFactory.CreateResourceGraph(options);
var targetedFields = new TargetedFields();

_jsonApiDeserializer = new RequestDeserializer(resourceGraph, new DefaultResourceFactory(new ServiceContainer()), targetedFields);
_jsonApiDeserializer = new RequestDeserializer(resourceGraph, new DefaultResourceFactory(new ServiceContainer()), targetedFields, new HttpContextAccessor());
}

[Benchmark]
Expand Down
2 changes: 2 additions & 0 deletions src/Examples/JsonApiDotNetCoreExample/Models/Article.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Models.CustomValidators;

namespace JsonApiDotNetCoreExample.Models
{
public sealed class Article : Identifiable
{
[Attr]
[IsRequired(AllowEmptyStrings = true)]
public string Name { get; set; }

[HasOne]
Expand Down
2 changes: 2 additions & 0 deletions src/Examples/JsonApiDotNetCoreExample/Models/Author.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using JsonApiDotNetCore.Models;
using System.Collections.Generic;
using JsonApiDotNetCore.Models.CustomValidators;

namespace JsonApiDotNetCoreExample.Models
{
public sealed class Author : Identifiable
{
[Attr]
[IsRequired(AllowEmptyStrings = true)]
public string Name { get; set; }

[HasMany]
Expand Down
1 change: 1 addition & 0 deletions src/Examples/ReportsExample/ReportsExample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="$(NpgsqlPostgreSQLVersion)" />
</ItemGroup>
</Project>
12 changes: 12 additions & 0 deletions src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,17 @@ internal static void SetJsonApiRequest(this HttpContext httpContext)
{
httpContext.Items["IsJsonApiRequest"] = bool.TrueString;
}

internal static void DisableValidator(this HttpContext httpContext, string propertyName, string model)
{
var itemKey = $"JsonApiDotNetCore_DisableValidation_{model}_{propertyName}";
httpContext.Items[itemKey] = true;
}

internal static bool IsValidatorDisabled(this HttpContext httpContext, string propertyName, string model)
{
return httpContext.Items.ContainsKey($"JsonApiDotNetCore_DisableValidation_{model}_{propertyName}") ||
httpContext.Items.ContainsKey($"JsonApiDotNetCore_DisableValidation_{model}_Relation");
}
}
}
2 changes: 1 addition & 1 deletion src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<VersionPrefix>4.0.0</VersionPrefix>
<TargetFramework>$(NetCoreAppVersion)</TargetFramework>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using System.ComponentModel.DataAnnotations;
using JsonApiDotNetCore.Extensions;

namespace JsonApiDotNetCore.Models.CustomValidators
{
public class IsRequiredAttribute : RequiredAttribute
{
private bool _isDisabled;

public override bool IsValid(object value)
{
return _isDisabled || base.IsValid(value);
}

protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var httpContextAccessor = (IHttpContextAccessor)validationContext.GetRequiredService(typeof(IHttpContextAccessor));
_isDisabled = httpContextAccessor.HttpContext.IsValidatorDisabled(validationContext.MemberName, validationContext.ObjectType.Name);
return _isDisabled ? ValidationResult.Success : base.IsValid(value, validationContext);
}
}
}
20 changes: 9 additions & 11 deletions src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,31 +69,30 @@ protected object Deserialize(string body)
/// <param name="attributeValues">Attributes and their values, as in the serialized content</param>
/// <param name="attributes">Exposed attributes for <paramref name="entity"/></param>
/// <returns></returns>
protected IIdentifiable SetAttributes(IIdentifiable entity, Dictionary<string, object> attributeValues, List<AttrAttribute> attributes)
protected virtual IIdentifiable SetAttributes(IIdentifiable entity, Dictionary<string, object> attributeValues, List<AttrAttribute> attributes)
{
if (attributeValues == null || attributeValues.Count == 0)
return entity;

foreach (var attr in attributes)
{
return entity;

foreach (AttrAttribute attr in attributes)
if (attributeValues.TryGetValue(attr.PublicAttributeName, out object newValue))
{
var convertedValue = ConvertAttrValue(newValue, attr.PropertyInfo.PropertyType);
attr.SetValue(entity, convertedValue);
AfterProcessField(entity, attr);
}
}

return entity;
}

/// <summary>
/// Sets the relationships on a parsed entity
/// </summary>
/// <param name="entity">The parsed entity</param>
/// <param name="relationshipsValues">Relationships and their values, as in the serialized content</param>
/// <param name="relationshipAttributes">Exposed relationships for <paramref name="entity"/></param>
/// <returns></returns>
protected IIdentifiable SetRelationships(IIdentifiable entity, Dictionary<string, RelationshipEntry> relationshipsValues, List<RelationshipAttribute> relationshipAttributes)
protected virtual IIdentifiable SetRelationships(IIdentifiable entity, Dictionary<string, RelationshipEntry> relationshipsValues, List<RelationshipAttribute> relationshipAttributes)
{
if (relationshipsValues == null || relationshipsValues.Count == 0)
return entity;
Expand All @@ -108,7 +107,6 @@ protected IIdentifiable SetRelationships(IIdentifiable entity, Dictionary<string
SetHasOneRelationship(entity, entityProperties, hasOneAttribute, relationshipData);
else
SetHasManyRelationship(entity, (HasManyAttribute)attr, relationshipData);

}
return entity;
}
Expand Down Expand Up @@ -161,7 +159,7 @@ private IIdentifiable ParseResourceObject(ResourceObject data)
/// <param name="entityProperties"></param>
/// <param name="attr"></param>
/// <param name="relationshipData"></param>
private void SetHasOneRelationship(IIdentifiable entity,
protected void SetHasOneRelationship(IIdentifiable entity,
PropertyInfo[] entityProperties,
HasOneAttribute attr,
RelationshipEntry relationshipData)
Expand Down Expand Up @@ -224,7 +222,7 @@ private void SetNavigation(IIdentifiable entity, HasOneAttribute attr, string re
/// <summary>
/// Sets a HasMany relationship.
/// </summary>
private void SetHasManyRelationship(
protected void SetHasManyRelationship(
IIdentifiable entity,
HasManyAttribute attr,
RelationshipEntry relationshipData)
Expand All @@ -245,7 +243,7 @@ private void SetHasManyRelationship(
AfterProcessField(entity, attr, relationshipData);
}

private object ConvertAttrValue(object newValue, Type targetType)
protected object ConvertAttrValue(object newValue, Type targetType)
{
if (newValue is JContainer jObject)
// the attribute value is a complex type that needs additional deserialization
Expand Down
65 changes: 63 additions & 2 deletions src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
using System;
using JsonApiDotNetCore.Exceptions;
using JsonApiDotNetCore.Internal;
using JsonApiDotNetCore.Internal.Contracts;
using JsonApiDotNetCore.Models;
using Microsoft.AspNetCore.Http;
using System.Collections.Generic;
using System.Reflection;
using JsonApiDotNetCore.Extensions;
using JsonApiDotNetCore.Models.CustomValidators;
using System.Net.Http;

namespace JsonApiDotNetCore.Serialization.Server
{
Expand All @@ -12,11 +17,13 @@ namespace JsonApiDotNetCore.Serialization.Server
public class RequestDeserializer : BaseDocumentParser, IJsonApiDeserializer
{
private readonly ITargetedFields _targetedFields;
private readonly IHttpContextAccessor _httpContextAccessor;

public RequestDeserializer(IResourceContextProvider contextProvider, IResourceFactory resourceFactory, ITargetedFields targetedFields)
public RequestDeserializer(IResourceContextProvider contextProvider, IResourceFactory resourceFactory, ITargetedFields targetedFields, IHttpContextAccessor httpContextAccessor)
: base(contextProvider, resourceFactory)
{
_targetedFields = targetedFields;
_httpContextAccessor = httpContextAccessor;
}

/// <inheritdoc/>
Expand Down Expand Up @@ -50,5 +57,59 @@ protected override void AfterProcessField(IIdentifiable entity, IResourceField f
else if (field is RelationshipAttribute relationship)
_targetedFields.Relationships.Add(relationship);
}

protected override IIdentifiable SetAttributes(IIdentifiable entity, Dictionary<string, object> attributeValues, List<AttrAttribute> attributes)
{
foreach (AttrAttribute attr in attributes)
{
var disableValidator = false;
if (attributeValues == null || attributeValues.Count == 0)
{
disableValidator = true;
}
else
{
if (attributeValues.TryGetValue(attr.PublicAttributeName, out object newValue))
{
object convertedValue = ConvertAttrValue(newValue, attr.PropertyInfo.PropertyType);
attr.SetValue(entity, convertedValue);
AfterProcessField(entity, attr);
}
else
{
disableValidator = true;
}
}

if (!disableValidator) continue;
if (_httpContextAccessor?.HttpContext?.Request.Method != HttpMethod.Patch.Method) continue;
if (attr.PropertyInfo.GetCustomAttribute<IsRequiredAttribute>() != null)
_httpContextAccessor?.HttpContext.DisableValidator(attr.PropertyInfo.Name,
entity.GetType().Name);
}

return entity;
}

protected override IIdentifiable SetRelationships(IIdentifiable entity, Dictionary<string, RelationshipEntry> relationshipsValues, List<RelationshipAttribute> relationshipAttributes)
{
if (relationshipsValues == null || relationshipsValues.Count == 0)
return entity;

var entityProperties = entity.GetType().GetProperties();
foreach (RelationshipAttribute attr in relationshipAttributes)
{
_httpContextAccessor?.HttpContext?.DisableValidator("Relation", attr.PropertyInfo.Name);

if (!relationshipsValues.TryGetValue(attr.PublicRelationshipName, out RelationshipEntry relationshipData) || !relationshipData.IsPopulated)
continue;

if (attr is HasOneAttribute hasOneAttribute)
SetHasOneRelationship(entity, entityProperties, hasOneAttribute, relationshipData);
else
SetHasManyRelationship(entity, (HasManyAttribute)attr, relationshipData);
}
return entity;
}
}
}
21 changes: 15 additions & 6 deletions test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,23 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance
[Collection("WebHostCollection")]
public sealed class ManyToManyTests
{
private readonly Faker<Article> _articleFaker = new Faker<Article>()
.RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10))
.RuleFor(a => a.Author, f => new Author());
private readonly TestFixture<TestStartup> _fixture;

private readonly Faker<Author> _authorFaker;
private readonly Faker<Article> _articleFaker;
private readonly Faker<Tag> _tagFaker;

private readonly TestFixture<TestStartup> _fixture;

public ManyToManyTests(TestFixture<TestStartup> fixture)
{
_fixture = fixture;

_authorFaker = new Faker<Author>()
.RuleFor(a => a.Name, f => f.Random.Words(2));

_articleFaker = new Faker<Article>()
.RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10))
.RuleFor(a => a.Author, f => _authorFaker.Generate());

_tagFaker = new Faker<Tag>()
.CustomInstantiator(f => new Tag(_fixture.GetService<AppDbContext>()))
.RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10));
Expand Down Expand Up @@ -282,7 +287,7 @@ public async Task Can_Create_Many_To_Many()
// Arrange
var context = _fixture.GetService<AppDbContext>();
var tag = _tagFaker.Generate();
var author = new Author();
var author = _authorFaker.Generate();
context.Tags.Add(tag);
context.AuthorDifferentDbContextName.Add(author);
await context.SaveChangesAsync();
Expand All @@ -294,6 +299,10 @@ public async Task Can_Create_Many_To_Many()
data = new
{
type = "articles",
attributes = new Dictionary<string, object>
{
{"name", "An article with relationships"}
},
relationships = new Dictionary<string, dynamic>
{
{ "author", new {
Expand Down
Loading