Skip to content

Commit f72615f

Browse files
maureiBart Koelman
and
Bart Koelman
authored
Deprecation of IsRequiredAttribute (#847)
* feat: JsonApiModelValidatorProvider * feat: working approach * chore: cleanup * chore: unused directives * chore: self review * fix: failing tests * fix * fix * fix: cleanup * fix: comments * chore: rm whitespace * fix: improve comment * fix: build * temp * feat: implemented * feat: implemented * fix: self review * fix: remove unused extensions * fix * fix: docs * fix: undo * fix: move files * - Simplified injection - Only inject when ModelState active - Added support for ID validation * Removed unneeded override * Fix filename casing 1/2 * Fix filename casing 2/2 * Removed unused usings Co-authored-by: Bart Koelman <[email protected]>
1 parent 65f9b49 commit f72615f

File tree

13 files changed

+178
-175
lines changed

13 files changed

+178
-175
lines changed

src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using JsonApiDotNetCore.Services;
1818
using Microsoft.AspNetCore.Http;
1919
using Microsoft.AspNetCore.Mvc;
20+
using Microsoft.AspNetCore.Mvc.ModelBinding;
2021
using Microsoft.EntityFrameworkCore;
2122
using Microsoft.Extensions.DependencyInjection;
2223
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -103,6 +104,7 @@ public void ConfigureMvc()
103104
if (_options.ValidateModelState)
104105
{
105106
_mvcBuilder.AddDataAnnotations();
107+
_services.AddSingleton<IModelMetadataProvider, JsonApiModelMetadataProvider>();
106108
}
107109
}
108110

@@ -163,7 +165,7 @@ private void AddMiddlewareLayer()
163165
_services.TryAddSingleton<IJsonApiRoutingConvention, JsonApiRoutingConvention>();
164166
_services.AddSingleton<IControllerResourceMapping>(sp => sp.GetRequiredService<IJsonApiRoutingConvention>());
165167
_services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
166-
_services.AddScoped<IRequestScopedServiceProvider, RequestScopedServiceProvider>();
168+
_services.AddSingleton<IRequestScopedServiceProvider, RequestScopedServiceProvider>();
167169
_services.AddScoped<IJsonApiRequest, JsonApiRequest>();
168170
_services.AddScoped<IJsonApiWriter, JsonApiWriter>();
169171
_services.AddScoped<IJsonApiReader, JsonApiReader>();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using Microsoft.AspNetCore.Mvc.ModelBinding;
3+
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
4+
using Microsoft.Extensions.Options;
5+
6+
namespace JsonApiDotNetCore.Configuration
7+
{
8+
/// <summary>
9+
/// Custom implementation of <see cref="IModelMetadataProvider"/> to support json:api partial patching.
10+
/// </summary>
11+
internal class JsonApiModelMetadataProvider : DefaultModelMetadataProvider
12+
{
13+
private readonly JsonApiValidationFilter _jsonApiValidationFilter;
14+
15+
/// <inheritdoc />
16+
public JsonApiModelMetadataProvider(ICompositeMetadataDetailsProvider detailsProvider, IRequestScopedServiceProvider serviceProvider)
17+
: base(detailsProvider)
18+
{
19+
_jsonApiValidationFilter = new JsonApiValidationFilter(serviceProvider);
20+
}
21+
22+
/// <inheritdoc />
23+
public JsonApiModelMetadataProvider(ICompositeMetadataDetailsProvider detailsProvider, IOptions<MvcOptions> optionsAccessor, IRequestScopedServiceProvider serviceProvider)
24+
: base(detailsProvider, optionsAccessor)
25+
{
26+
_jsonApiValidationFilter = new JsonApiValidationFilter(serviceProvider);
27+
}
28+
29+
/// <inheritdoc />
30+
protected override ModelMetadata CreateModelMetadata(DefaultMetadataDetails entry)
31+
{
32+
var metadata = (DefaultModelMetadata)base.CreateModelMetadata(entry);
33+
34+
if (metadata.ValidationMetadata.IsRequired == true)
35+
{
36+
metadata.ValidationMetadata.PropertyValidationFilter = _jsonApiValidationFilter;
37+
}
38+
39+
return metadata;
40+
}
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System;
2+
using System.Linq;
3+
using JsonApiDotNetCore.Middleware;
4+
using JsonApiDotNetCore.Resources;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
7+
using Microsoft.Extensions.DependencyInjection;
8+
9+
namespace JsonApiDotNetCore.Configuration
10+
{
11+
/// <summary>
12+
/// Validation filter that blocks ASP.NET Core ModelState validation on data according to the json:api spec.
13+
/// </summary>
14+
internal sealed class JsonApiValidationFilter : IPropertyValidationFilter
15+
{
16+
private readonly IRequestScopedServiceProvider _serviceProvider;
17+
18+
public JsonApiValidationFilter(IRequestScopedServiceProvider serviceProvider)
19+
{
20+
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
21+
}
22+
23+
/// <inheritdoc />
24+
public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEntry)
25+
{
26+
var request = _serviceProvider.GetRequiredService<IJsonApiRequest>();
27+
28+
if (IsId(entry.Key))
29+
{
30+
return true;
31+
}
32+
33+
var isTopResourceInPrimaryRequest = string.IsNullOrEmpty(parentEntry.Key) && request.Kind == EndpointKind.Primary;
34+
if (!isTopResourceInPrimaryRequest)
35+
{
36+
return false;
37+
}
38+
39+
var httpContextAccessor = _serviceProvider.GetRequiredService<IHttpContextAccessor>();
40+
if (httpContextAccessor.HttpContext.Request.Method == HttpMethods.Patch)
41+
{
42+
var targetedFields = _serviceProvider.GetRequiredService<ITargetedFields>();
43+
return IsFieldTargeted(entry, targetedFields);
44+
}
45+
46+
return true;
47+
}
48+
49+
private static bool IsId(string key)
50+
{
51+
return key == nameof(Identifiable.Id) || key.EndsWith("." + nameof(Identifiable.Id), StringComparison.Ordinal);
52+
}
53+
54+
private static bool IsFieldTargeted(ValidationEntry entry, ITargetedFields targetedFields)
55+
{
56+
return targetedFields.Attributes.Any(attribute => attribute.Property.Name == entry.Key);
57+
}
58+
}
59+
}

src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs

-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.ComponentModel.Design;
43
using System.Linq;
54
using System.Reflection;
65
using JsonApiDotNetCore.Errors;
7-
using JsonApiDotNetCore.Resources;
86
using JsonApiDotNetCore.Serialization.Building;
97
using JsonApiDotNetCore.Serialization.Client.Internal;
108
using JsonApiDotNetCore.Services;

src/JsonApiDotNetCore/Middleware/HttpContextExtensions.cs

-20
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ namespace JsonApiDotNetCore.Middleware
66
public static class HttpContextExtensions
77
{
88
private const string _isJsonApiRequestKey = "JsonApiDotNetCore_IsJsonApiRequest";
9-
private const string _disableRequiredValidatorKey = "JsonApiDotNetCore_DisableRequiredValidator";
109

1110
/// <summary>
1211
/// Indicates whether the currently executing HTTP request is being handled by JsonApiDotNetCore.
@@ -25,24 +24,5 @@ internal static void RegisterJsonApiRequest(this HttpContext httpContext)
2524

2625
httpContext.Items[_isJsonApiRequestKey] = bool.TrueString;
2726
}
28-
29-
internal static void DisableRequiredValidator(this HttpContext httpContext, string propertyName, string model)
30-
{
31-
if (httpContext == null) throw new ArgumentNullException(nameof(httpContext));
32-
if (propertyName == null) throw new ArgumentNullException(nameof(propertyName));
33-
if (model == null) throw new ArgumentNullException(nameof(model));
34-
35-
var itemKey = $"{_disableRequiredValidatorKey}_{model}_{propertyName}";
36-
httpContext.Items[itemKey] = true;
37-
}
38-
39-
internal static bool IsRequiredValidatorDisabled(this HttpContext httpContext, string propertyName, string model)
40-
{
41-
if (httpContext == null) throw new ArgumentNullException(nameof(httpContext));
42-
if (propertyName == null) throw new ArgumentNullException(nameof(propertyName));
43-
if (model == null) throw new ArgumentNullException(nameof(model));
44-
45-
return httpContext.Items.ContainsKey($"{_disableRequiredValidatorKey}_{model}_{propertyName}");
46-
}
4727
}
4828
}

src/JsonApiDotNetCore/Resources/Annotations/IsRequiredAttribute.cs

-117
This file was deleted.

src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ protected object DeserializeBody(string body)
6868
/// <param name="resource">The parsed resource.</param>
6969
/// <param name="attributeValues">Attributes and their values, as in the serialized content.</param>
7070
/// <param name="attributes">Exposed attributes for <paramref name="resource"/>.</param>
71-
protected virtual IIdentifiable SetAttributes(IIdentifiable resource, IDictionary<string, object> attributeValues, IReadOnlyCollection<AttrAttribute> attributes)
71+
protected IIdentifiable SetAttributes(IIdentifiable resource, IDictionary<string, object> attributeValues, IReadOnlyCollection<AttrAttribute> attributes)
7272
{
7373
if (resource == null) throw new ArgumentNullException(nameof(resource));
7474
if (attributes == null) throw new ArgumentNullException(nameof(attributes));

src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs

-27
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
using System;
2-
using System.Collections.Generic;
32
using System.Net.Http;
4-
using System.Reflection;
53
using JsonApiDotNetCore.Configuration;
64
using JsonApiDotNetCore.Errors;
7-
using JsonApiDotNetCore.Middleware;
85
using JsonApiDotNetCore.Resources;
96
using JsonApiDotNetCore.Resources.Annotations;
107
using JsonApiDotNetCore.Serialization.Objects;
@@ -67,29 +64,5 @@ protected override void AfterProcessField(IIdentifiable resource, ResourceFieldA
6764
else if (field is RelationshipAttribute relationship)
6865
_targetedFields.Relationships.Add(relationship);
6966
}
70-
71-
protected override IIdentifiable SetAttributes(IIdentifiable resource, IDictionary<string, object> attributeValues, IReadOnlyCollection<AttrAttribute> attributes)
72-
{
73-
if (resource == null) throw new ArgumentNullException(nameof(resource));
74-
if (attributes == null) throw new ArgumentNullException(nameof(attributes));
75-
76-
if (_httpContextAccessor.HttpContext.Request.Method == HttpMethod.Patch.Method)
77-
{
78-
foreach (AttrAttribute attr in attributes)
79-
{
80-
if (attr.Property.GetCustomAttribute<IsRequiredAttribute>() != null)
81-
{
82-
bool disableValidator = attributeValues == null || !attributeValues.ContainsKey(attr.PublicName);
83-
84-
if (disableValidator)
85-
{
86-
_httpContextAccessor.HttpContext.DisableRequiredValidator(attr.Property.Name, resource.GetType().Name);
87-
}
88-
}
89-
}
90-
}
91-
92-
return base.SetAttributes(resource, attributeValues, attributes);
93-
}
9467
}
9568
}

0 commit comments

Comments
 (0)