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 23 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
10 changes: 10 additions & 0 deletions src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@
using JsonApiDotNetCore.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace JsonApiDotNetCore.Configuration
{
Expand Down Expand Up @@ -169,6 +172,13 @@ private void AddMiddlewareLayer()
_services.AddScoped<IJsonApiReader, JsonApiReader>();
_services.AddScoped<ITargetedFields, TargetedFields>();
_services.AddScoped<IFieldsToSerialize, FieldsToSerialize>();
_services.AddSingleton<IModelMetadataProvider, JsonApiModelMetadataProvider>();
_services.AddSingleton<IObjectModelValidator>(s =>
{
var options = s.GetRequiredService<IOptions<MvcOptions>>().Value;
var metadataProvider = s.GetRequiredService<IModelMetadataProvider>();
return new JsonApiObjectValidator(metadataProvider, options.ModelValidatorProviders, options);
});
}

private void AddResourceLayer()
Expand Down
38 changes: 38 additions & 0 deletions src/JsonApiDotNetCore/Configuration/JsonApiMetaDataProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Options;

namespace JsonApiDotNetCore.Configuration
{
/// <summary>
/// Custom implementation of <see cref="IModelMetadataProvider"/> that sets an additional <see cref="IPropertyValidationFilter"/>
/// to support partial patching.
/// </summary>
internal class JsonApiModelMetadataProvider : DefaultModelMetadataProvider
{
/// <inheritdoc />
public JsonApiModelMetadataProvider(ICompositeMetadataDetailsProvider detailsProvider)
: base(detailsProvider) { }

/// <inheritdoc />
public JsonApiModelMetadataProvider(ICompositeMetadataDetailsProvider detailsProvider, IOptions<MvcOptions> optionsAccessor)
: base(detailsProvider, optionsAccessor) { }

/// <inheritdoc />
protected override ModelMetadata CreateModelMetadata(DefaultMetadataDetails entry)
{
var metadata = new DefaultModelMetadata(this, DetailsProvider, entry, ModelBindingMessageProvider) ;

var isRequired = metadata.ValidationMetadata.IsRequired;

if (isRequired != null && isRequired.Value)
{
metadata.ValidationMetadata.PropertyValidationFilter = new PartialPatchValidationFilter();
}

return metadata;
}
}
}
99 changes: 99 additions & 0 deletions src/JsonApiDotNetCore/Configuration/JsonApiObjectValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;

namespace JsonApiDotNetCore.Configuration
{
/// <summary>
/// Custom implementation of <see cref="IObjectModelValidator"/> that is identical to DefaultObjectValidator, apart from
/// using our own <see cref="JsonApiValidationVisitor"/> instead of the built-in <see cref="ValidationVisitor"/>.
/// </summary>
/// <remarks>
/// See https://github.com/dotnet/aspnetcore/blob/v3.1.8/src/Mvc/Mvc.Core/src/ModelBinding/Validation/DefaultObjectValidator.cs
/// </remarks>
internal class JsonApiObjectValidator : ObjectModelValidator
{
private readonly IModelMetadataProvider _modelMetadataProvider;
private readonly MvcOptions _mvcOptions;
private readonly ValidatorCache _validatorCache;
private readonly CompositeModelValidatorProvider _validatorProvider;

/// <inheritdoc />
public JsonApiObjectValidator(
IModelMetadataProvider modelMetadataProvider,
IList<IModelValidatorProvider> validatorProviders,
MvcOptions mvcOptions)
: base(modelMetadataProvider, validatorProviders)
{
if (validatorProviders == null)
{
throw new ArgumentNullException(nameof(validatorProviders));
}

_modelMetadataProvider = modelMetadataProvider ?? throw new ArgumentNullException(nameof(modelMetadataProvider));
_validatorCache = new ValidatorCache();
_validatorProvider = new CompositeModelValidatorProvider(validatorProviders);
_mvcOptions = mvcOptions;
}

/// <inheritdoc />
public override ValidationVisitor GetValidationVisitor(
ActionContext actionContext,
IModelValidatorProvider validatorProvider,
ValidatorCache validatorCache,
IModelMetadataProvider metadataProvider,
ValidationStateDictionary validationState)
{
var visitor = new JsonApiValidationVisitor(
actionContext,
validatorProvider,
validatorCache,
metadataProvider,
validationState)
{
MaxValidationDepth = _mvcOptions.MaxValidationDepth,
ValidateComplexTypesIfChildValidationFails = _mvcOptions.ValidateComplexTypesIfChildValidationFails,
};

return visitor;
}

/// <inheritdoc />
public override void Validate(
ActionContext actionContext,
ValidationStateDictionary validationState,
string prefix,
object model)
{
var visitor = GetValidationVisitor(
actionContext,
_validatorProvider,
_validatorCache,
_modelMetadataProvider,
validationState);

var metadata = model == null ? null : _modelMetadataProvider.GetMetadataForType(model.GetType());
visitor.Validate(metadata, prefix, model, alwaysValidateAtTopLevel: false);
}

/// <inheritdoc />
public override void Validate(
ActionContext actionContext,
ValidationStateDictionary validationState,
string prefix,
object model,
ModelMetadata metadata)
{
var visitor = GetValidationVisitor(
actionContext,
_validatorProvider,
_validatorCache,
_modelMetadataProvider,
validationState);

visitor.Validate(metadata, prefix, model, alwaysValidateAtTopLevel: metadata.IsRequired);
}
}
}
54 changes: 54 additions & 0 deletions src/JsonApiDotNetCore/Configuration/JsonApiValidationVisitor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;

namespace JsonApiDotNetCore.Configuration
{
/// <summary>
/// An extension of the internal <see cref="ValidationVisitor"/> that performs an additional check related to
/// property validation filters
/// </summary>
/// <remarks>
/// see https://github.com/dotnet/aspnetcore/issues/26580 for background information.
/// </remarks>
internal sealed class JsonApiValidationVisitor : ValidationVisitor
{
/// <inheritdoc />
public JsonApiValidationVisitor(
ActionContext actionContext,
IModelValidatorProvider validatorProvider,
ValidatorCache validatorCache,
IModelMetadataProvider metadataProvider,
ValidationStateDictionary validationState)
: base(actionContext, validatorProvider, validatorCache, metadataProvider, validationState) { }

/// <inheritdoc />
protected override bool VisitChildren(IValidationStrategy strategy)
{
var isValid = true;
var enumerator = strategy.GetChildren(Metadata, Key, Model);
var parentEntry = new ValidationEntry(Metadata, Key, Model);

while (enumerator.MoveNext())
{
var entry = enumerator.Current;
var metadata = entry.Metadata;
var key = entry.Key;

var jsonApiFilter = metadata.PropertyValidationFilter as PartialPatchValidationFilter;
var serviceProvider = Context?.HttpContext?.RequestServices;

if (metadata.PropertyValidationFilter?.ShouldValidateEntry(entry, parentEntry) == false
|| jsonApiFilter != null && jsonApiFilter.ShouldValidateEntry(entry, parentEntry, serviceProvider) == false )
{
SuppressValidation(key);
continue;
}

isValid &= Visit(metadata, key, entry.Model);
}

return isValid;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
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 enables partial patching as part of the json:api spec.
/// </summary>
internal sealed class PartialPatchValidationFilter : IPropertyValidationFilter
{
/// <inheritdoc />
public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEntry) => true;

public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEntry, IServiceProvider serviceProvider)
{
if (serviceProvider == null) throw new ArgumentException(nameof(serviceProvider));

var request = serviceProvider.GetRequiredService<IJsonApiRequest>();
var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
var targetedFields = serviceProvider.GetRequiredService<ITargetedFields>();

if (request.Kind == EndpointKind.Primary && string.IsNullOrEmpty(parentEntry.Key) && RequiredFieldIsTargeted(entry, targetedFields, httpContextAccessor))
{
return true;
}

return false;
}

private bool RequiredFieldIsTargeted(ValidationEntry entry, ITargetedFields targetedFields, IHttpContextAccessor httpContextAccessor)
{
var requestMethod = httpContextAccessor.HttpContext.Request.Method;

if (requestMethod == HttpMethods.Post)
{
return true;
}

if (requestMethod == HttpMethods.Patch)
{
foreach (var attribute in targetedFields.Attributes)
{
if (attribute.Property.Name == entry.Key)
{
return true;
}
}
}

return false;
}
}
}
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}");
}
}
}
Loading