Skip to content

Deprecation of IsRequiredAttribute #847

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 28 commits into from
Oct 5, 2020
Merged
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
Expand Up @@ -17,6 +17,7 @@
using JsonApiDotNetCore.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
Expand Down Expand Up @@ -103,6 +104,7 @@ public void ConfigureMvc()
if (_options.ValidateModelState)
{
_mvcBuilder.AddDataAnnotations();
_services.AddSingleton<IModelMetadataProvider, JsonApiModelMetadataProvider>();
}
}

Expand Down Expand Up @@ -163,7 +165,7 @@ private void AddMiddlewareLayer()
_services.TryAddSingleton<IJsonApiRoutingConvention, JsonApiRoutingConvention>();
_services.AddSingleton<IControllerResourceMapping>(sp => sp.GetRequiredService<IJsonApiRoutingConvention>());
_services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
_services.AddScoped<IRequestScopedServiceProvider, RequestScopedServiceProvider>();
_services.AddSingleton<IRequestScopedServiceProvider, RequestScopedServiceProvider>();
_services.AddScoped<IJsonApiRequest, JsonApiRequest>();
_services.AddScoped<IJsonApiWriter, JsonApiWriter>();
_services.AddScoped<IJsonApiReader, JsonApiReader>();
Expand Down
42 changes: 42 additions & 0 deletions src/JsonApiDotNetCore/Configuration/JsonApiMetadataProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.Options;

namespace JsonApiDotNetCore.Configuration
{
/// <summary>
/// Custom implementation of <see cref="IModelMetadataProvider"/> to support json:api partial patching.
/// </summary>
internal class JsonApiModelMetadataProvider : DefaultModelMetadataProvider
{
private readonly JsonApiValidationFilter _jsonApiValidationFilter;

/// <inheritdoc />
public JsonApiModelMetadataProvider(ICompositeMetadataDetailsProvider detailsProvider, IRequestScopedServiceProvider serviceProvider)
: base(detailsProvider)
{
_jsonApiValidationFilter = new JsonApiValidationFilter(serviceProvider);
}

/// <inheritdoc />
public JsonApiModelMetadataProvider(ICompositeMetadataDetailsProvider detailsProvider, IOptions<MvcOptions> optionsAccessor, IRequestScopedServiceProvider serviceProvider)
: base(detailsProvider, optionsAccessor)
{
_jsonApiValidationFilter = new JsonApiValidationFilter(serviceProvider);
}

/// <inheritdoc />
protected override ModelMetadata CreateModelMetadata(DefaultMetadataDetails entry)
{
var metadata = (DefaultModelMetadata)base.CreateModelMetadata(entry);

if (metadata.ValidationMetadata.IsRequired == true)
{
metadata.ValidationMetadata.PropertyValidationFilter = _jsonApiValidationFilter;
}

return metadata;
}
}
}
59 changes: 59 additions & 0 deletions src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;
using System.Linq;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Resources;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.DependencyInjection;

namespace JsonApiDotNetCore.Configuration
{
/// <summary>
/// Validation filter that blocks ASP.NET Core ModelState validation on data according to the json:api spec.
/// </summary>
internal sealed class JsonApiValidationFilter : IPropertyValidationFilter
{
private readonly IRequestScopedServiceProvider _serviceProvider;

public JsonApiValidationFilter(IRequestScopedServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}

/// <inheritdoc />
public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEntry)
{
var request = _serviceProvider.GetRequiredService<IJsonApiRequest>();

if (IsId(entry.Key))
{
return true;
}

var isTopResourceInPrimaryRequest = string.IsNullOrEmpty(parentEntry.Key) && request.Kind == EndpointKind.Primary;
if (!isTopResourceInPrimaryRequest)
{
return false;
}

var httpContextAccessor = _serviceProvider.GetRequiredService<IHttpContextAccessor>();
if (httpContextAccessor.HttpContext.Request.Method == HttpMethods.Patch)
{
var targetedFields = _serviceProvider.GetRequiredService<ITargetedFields>();
return IsFieldTargeted(entry, targetedFields);
}

return true;
}

private static bool IsId(string key)
{
return key == nameof(Identifiable.Id) || key.EndsWith("." + nameof(Identifiable.Id), StringComparison.Ordinal);
}

private static bool IsFieldTargeted(ValidationEntry entry, ITargetedFields targetedFields)
{
return targetedFields.Attributes.Any(attribute => attribute.Property.Name == entry.Key);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Linq;
using System.Reflection;
using JsonApiDotNetCore.Errors;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Serialization.Building;
using JsonApiDotNetCore.Serialization.Client.Internal;
using JsonApiDotNetCore.Services;
Expand Down
20 changes: 0 additions & 20 deletions src/JsonApiDotNetCore/Middleware/HttpContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ 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.
Expand All @@ -25,24 +24,5 @@ internal static void RegisterJsonApiRequest(this HttpContext httpContext)

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

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 = $"{_disableRequiredValidatorKey}_{model}_{propertyName}";
httpContext.Items[itemKey] = true;
}

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($"{_disableRequiredValidatorKey}_{model}_{propertyName}");
}
}
}
117 changes: 0 additions & 117 deletions src/JsonApiDotNetCore/Resources/Annotations/IsRequiredAttribute.cs

This file was deleted.

2 changes: 1 addition & 1 deletion src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ protected object DeserializeBody(string body)
/// <param name="resource">The parsed resource.</param>
/// <param name="attributeValues">Attributes and their values, as in the serialized content.</param>
/// <param name="attributes">Exposed attributes for <paramref name="resource"/>.</param>
protected virtual IIdentifiable SetAttributes(IIdentifiable resource, IDictionary<string, object> attributeValues, IReadOnlyCollection<AttrAttribute> attributes)
protected IIdentifiable SetAttributes(IIdentifiable resource, IDictionary<string, object> attributeValues, IReadOnlyCollection<AttrAttribute> attributes)
{
if (resource == null) throw new ArgumentNullException(nameof(resource));
if (attributes == null) throw new ArgumentNullException(nameof(attributes));
Expand Down
27 changes: 0 additions & 27 deletions src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Reflection;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Errors;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;
using JsonApiDotNetCore.Serialization.Objects;
Expand Down Expand Up @@ -67,29 +64,5 @@ protected override void AfterProcessField(IIdentifiable resource, ResourceFieldA
else if (field is RelationshipAttribute relationship)
_targetedFields.Relationships.Add(relationship);
}

protected override IIdentifiable SetAttributes(IIdentifiable resource, IDictionary<string, object> attributeValues, IReadOnlyCollection<AttrAttribute> attributes)
{
if (resource == null) throw new ArgumentNullException(nameof(resource));
if (attributes == null) throw new ArgumentNullException(nameof(attributes));

if (_httpContextAccessor.HttpContext.Request.Method == HttpMethod.Patch.Method)
{
foreach (AttrAttribute attr in attributes)
{
if (attr.Property.GetCustomAttribute<IsRequiredAttribute>() != null)
{
bool disableValidator = attributeValues == null || !attributeValues.ContainsKey(attr.PublicName);

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

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