Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 0 additions & 1 deletion src/Examples/JsonApiDotNetCoreExample/Models/Article.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ namespace JsonApiDotNetCoreExample.Models
public sealed class Article : Identifiable
{
[Attr]
[IsRequired(AllowEmptyStrings = true)]
public string Caption { get; set; }

[Attr]
Expand Down
1 change: 0 additions & 1 deletion src/Examples/JsonApiDotNetCoreExample/Models/Author.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ public sealed class Author : Identifiable
public string FirstName { get; set; }

[Attr]
[IsRequired(AllowEmptyStrings = true)]
public string LastName { get; set; }

[Attr]
Expand Down
2 changes: 0 additions & 2 deletions src/Examples/JsonApiDotNetCoreExample/Models/Tag.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.ComponentModel.DataAnnotations;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;

Expand All @@ -7,7 +6,6 @@ namespace JsonApiDotNetCoreExample.Models
public class Tag : Identifiable
{
[Attr]
[RegularExpression(@"^\W$")]
public string Name { get; set; }

[Attr]
Expand Down
18 changes: 14 additions & 4 deletions src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,7 @@ private static IReadOnlyCollection<Error> FromModelState(ModelStateDictionary mo

foreach (var (propertyName, entry) in modelState.Where(x => x.Value.Errors.Any()))
{
PropertyInfo property = resourceType.GetProperty(propertyName);

string attributeName =
property.GetCustomAttribute<AttrAttribute>().PublicName ?? namingStrategy.GetPropertyName(property.Name, false);
string attributeName = GetDisplayNameForProperty(propertyName, resourceType, namingStrategy);

foreach (var modelError in entry.Errors)
{
Expand All @@ -55,6 +52,19 @@ private static IReadOnlyCollection<Error> FromModelState(ModelStateDictionary mo
return errors;
}

private static string GetDisplayNameForProperty(string propertyName, Type resourceType,
NamingStrategy namingStrategy)
{
PropertyInfo property = resourceType.GetProperty(propertyName);
if (property != null)
{
var attrAttribute = property.GetCustomAttribute<AttrAttribute>();
return attrAttribute?.PublicName ?? namingStrategy.GetPropertyName(property.Name, false);
}

return propertyName;
}

private static Error FromModelError(ModelError modelError, string attributeName,
bool includeExceptionStackTraceInErrors)
{
Expand Down
16 changes: 9 additions & 7 deletions src/JsonApiDotNetCore/Middleware/HttpContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,44 @@ namespace JsonApiDotNetCore.Middleware
{
public static class HttpContextExtensions
{
private const string _isJsonApiRequestKey = "JsonApiDotNetCore_IsJsonApiRequest";
private const string _disableRequiredValidatorKey = "JsonApiDotNetCore_DisableRequiredValidator";

/// <summary>
/// Indicates whether the currently executing HTTP request is being handled by JsonApiDotNetCore.
/// </summary>
public static bool IsJsonApiRequest(this HttpContext httpContext)
{
if (httpContext == null) throw new ArgumentNullException(nameof(httpContext));

string value = httpContext.Items["IsJsonApiRequest"] as string;
string value = httpContext.Items[_isJsonApiRequestKey] as string;
return value == bool.TrueString;
}

internal static void RegisterJsonApiRequest(this HttpContext httpContext)
{
if (httpContext == null) throw new ArgumentNullException(nameof(httpContext));

httpContext.Items["IsJsonApiRequest"] = bool.TrueString;
httpContext.Items[_isJsonApiRequestKey] = bool.TrueString;
}

internal static void DisableValidator(this HttpContext httpContext, string propertyName, string model)
internal static void DisableRequiredValidator(this HttpContext httpContext, string propertyName, string model)
{
if (httpContext == null) throw new ArgumentNullException(nameof(httpContext));
if (propertyName == null) throw new ArgumentNullException(nameof(propertyName));
if (model == null) throw new ArgumentNullException(nameof(model));

var itemKey = $"JsonApiDotNetCore_DisableValidation_{model}_{propertyName}";
var itemKey = _disableRequiredValidatorKey + $"_{model}_{propertyName}";
httpContext.Items[itemKey] = true;
}

internal static bool IsValidatorDisabled(this HttpContext httpContext, string propertyName, string model)
internal static bool IsRequiredValidatorDisabled(this HttpContext httpContext, string propertyName, string model)
{
if (httpContext == null) throw new ArgumentNullException(nameof(httpContext));
if (propertyName == null) throw new ArgumentNullException(nameof(propertyName));
if (model == null) throw new ArgumentNullException(nameof(model));

return httpContext.Items.ContainsKey($"JsonApiDotNetCore_DisableValidation_{model}_{propertyName}") ||
httpContext.Items.ContainsKey($"JsonApiDotNetCore_DisableValidation_{model}_Relation");
return httpContext.Items.ContainsKey(_disableRequiredValidatorKey + $"_{model}_{propertyName}");
}
}
}
48 changes: 39 additions & 9 deletions src/JsonApiDotNetCore/Resources/Annotations/IsRequiredAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,52 @@ namespace JsonApiDotNetCore.Resources.Annotations
/// </summary>
public sealed class IsRequiredAttribute : RequiredAttribute
{
private bool _isDisabled;
public override bool RequiresValidationContext => true;

/// <inheritdoc />
public override bool IsValid(object value)
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
return _isDisabled || base.IsValid(value);
if (validationContext == null) throw new ArgumentNullException(nameof(validationContext));

var request = validationContext.GetRequiredService<IJsonApiRequest>();
var httpContextAccessor = validationContext.GetRequiredService<IHttpContextAccessor>();

if (ShouldSkipValidationForResource(validationContext, request) ||
ShouldSkipValidationForProperty(validationContext, httpContextAccessor.HttpContext))
{
return ValidationResult.Success;
}

return base.IsValid(value, validationContext);
}

/// <inheritdoc />
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
private static bool ShouldSkipValidationForResource(ValidationContext validationContext, IJsonApiRequest request)
{
if (validationContext == null) throw new ArgumentNullException(nameof(validationContext));
if (request.Kind == EndpointKind.Primary)
{
// If there is a relationship included in the data of the POST or PATCH, then the 'IsRequired' attribute will be disabled for any
// property within that object. For instance, a new article is posted and has a relationship included to an author. In this case,
// the author name (which has the 'IsRequired' attribute) will not be included in the POST. Unless disabled, the POST will fail.

if (validationContext.ObjectType != request.PrimaryResource.ResourceType)
{
return true;
}

if (validationContext.ObjectInstance is IIdentifiable identifiable &&
identifiable.StringId != request.PrimaryId)
{
return true;
}
}

var httpContextAccessor = (IHttpContextAccessor)validationContext.GetRequiredService(typeof(IHttpContextAccessor));
_isDisabled = httpContextAccessor.HttpContext.IsValidatorDisabled(validationContext.MemberName, validationContext.ObjectType.Name);
return _isDisabled ? ValidationResult.Success : base.IsValid(value, validationContext);
return false;
}

private static bool ShouldSkipValidationForProperty(ValidationContext validationContext, HttpContext httpContext)
{
return httpContext.IsRequiredValidatorDisabled(validationContext.MemberName,
validationContext.ObjectType.Name);
}
}
}
21 changes: 2 additions & 19 deletions src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,34 +72,17 @@ protected override IIdentifiable SetAttributes(IIdentifiable resource, IDictiona
{
if (attr.Property.GetCustomAttribute<IsRequiredAttribute>() != null)
{
bool disableValidator = attributeValues == null || attributeValues.Count == 0 ||
!attributeValues.TryGetValue(attr.PublicName, out _);
bool disableValidator = attributeValues == null || !attributeValues.ContainsKey(attr.PublicName);

if (disableValidator)
{
_httpContextAccessor.HttpContext.DisableValidator(attr.Property.Name, resource.GetType().Name);
_httpContextAccessor.HttpContext.DisableRequiredValidator(attr.Property.Name, resource.GetType().Name);
}
}
}
}

return base.SetAttributes(resource, attributeValues, attributes);
}

protected override IIdentifiable SetRelationships(IIdentifiable resource, IDictionary<string, RelationshipEntry> relationshipsValues, IReadOnlyCollection<RelationshipAttribute> relationshipAttributes)
{
if (resource == null) throw new ArgumentNullException(nameof(resource));
if (relationshipAttributes == null) throw new ArgumentNullException(nameof(relationshipAttributes));

// If there is a relationship included in the data of the POST or PATCH, then the 'IsRequired' attribute will be disabled for any
// property within that object. For instance, a new article is posted and has a relationship included to an author. In this case,
// the author name (which has the 'IsRequired' attribute) will not be included in the POST. Unless disabled, the POST will fail.
foreach (RelationshipAttribute attr in relationshipAttributes)
{
_httpContextAccessor.HttpContext.DisableValidator("Relation", attr.Property.Name);
}

return base.SetRelationships(resource, relationshipsValues, relationshipAttributes);
}
}
}
Loading