Skip to content
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
5 changes: 5 additions & 0 deletions src/Mvc/Mvc.Abstractions/src/ModelBinding/ModelMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,11 @@ internal IReadOnlyDictionary<ModelMetadata, ModelMetadata> BoundConstructorPrope
/// </summary>
internal virtual bool PropertyHasValidators => false;

/// <summary>
/// Gets the name of a model, if specified explicitly, to be used on <see cref="ValidationEntry"/>
/// </summary>
internal virtual string? ValidationModelName { get; }

/// <summary>
/// Throws if the ModelMetadata is for a record type with validation on properties.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,9 @@ public override bool? HasValidators

internal override bool PropertyHasValidators => ValidationMetadata.PropertyHasValidators;

/// <inheritdoc />
internal override string? ValidationModelName => ValidationMetadata.ValidationModelName;

internal static bool CalculateHasValidators(HashSet<DefaultModelMetadata> visited, ModelMetadata metadata)
{
RuntimeHelpers.EnsureSufficientExecutionStack();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable

using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;

/// <summary>
/// An implementation of <see cref="IDisplayMetadataProvider"/> and <see cref="IValidationMetadataProvider"/> for
/// the System.Text.Json.Serialization attribute classes.
/// </summary>
public sealed class SystemTextJsonValidationMetadataProvider : IDisplayMetadataProvider, IValidationMetadataProvider
{
private readonly JsonNamingPolicy _jsonNamingPolicy;

/// <summary>
/// Creates a new <see cref="SystemTextJsonValidationMetadataProvider"/> with the default <see cref="JsonNamingPolicy.CamelCase"/>
/// </summary>
public SystemTextJsonValidationMetadataProvider()
: this(JsonNamingPolicy.CamelCase)
{ }

/// <summary>
/// Creates a new <see cref="SystemTextJsonValidationMetadataProvider"/> with an optional <see cref="JsonNamingPolicy"/>
/// </summary>
/// <param name="namingPolicy">The <see cref="JsonNamingPolicy"/> to be used to configure the metadata provider.</param>
public SystemTextJsonValidationMetadataProvider(JsonNamingPolicy namingPolicy)
{
if (namingPolicy == null)
{
throw new ArgumentNullException(nameof(namingPolicy));
}

_jsonNamingPolicy = namingPolicy;
}

/// <inheritdoc />
public void CreateDisplayMetadata(DisplayMetadataProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

var propertyName = ReadPropertyNameFrom(context.Attributes);

if (!string.IsNullOrEmpty(propertyName))
{
context.DisplayMetadata.DisplayName = () => propertyName;
}
}

/// <inheritdoc />
public void CreateValidationMetadata(ValidationMetadataProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

var propertyName = ReadPropertyNameFrom(context.Attributes);

if (string.IsNullOrEmpty(propertyName))
{
propertyName = _jsonNamingPolicy.ConvertName(context.Key.Name!);
}

context.ValidationMetadata.ValidationModelName = propertyName;
}

private static string? ReadPropertyNameFrom(IReadOnlyList<object> attributes)
=> attributes?.OfType<JsonPropertyNameAttribute>().FirstOrDefault()?.Name;
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,9 @@ public class ValidationMetadata
/// Gets or sets a value that determines if validators can be constructed using metadata on properties.
/// </summary>
internal bool PropertyHasValidators { get; set; }

/// <summary>
/// Gets or sets a model name that will be used in <see cref="ValidationEntry"/>.
/// </summary>
public string? ValidationModelName { get; set; }
Comment thread
brunolins16 marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public bool MoveNext()
else
{
var property = _properties[_index - _parameters.Count];
var propertyName = property.BinderModelName ?? property.PropertyName;
var propertyName = property.ValidationModelName ?? property.BinderModelName ?? property.PropertyName;
Comment thread
brunolins16 marked this conversation as resolved.
var key = ModelNames.CreatePropertyModelName(_key, propertyName);

if (_model == null)
Expand Down
7 changes: 7 additions & 0 deletions src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
#nullable enable
Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.SystemTextJsonValidationMetadataProvider
Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.SystemTextJsonValidationMetadataProvider.CreateDisplayMetadata(Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.DisplayMetadataProviderContext! context) -> void
Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.SystemTextJsonValidationMetadataProvider.CreateValidationMetadata(Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ValidationMetadataProviderContext! context) -> void
Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.SystemTextJsonValidationMetadataProvider.SystemTextJsonValidationMetadataProvider() -> void
Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.SystemTextJsonValidationMetadataProvider.SystemTextJsonValidationMetadataProvider(System.Text.Json.JsonNamingPolicy! namingPolicy) -> void
Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ValidationMetadata.ValidationModelName.get -> string?
Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ValidationMetadata.ValidationModelName.set -> void
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Json;
using System.Text.Json.Serialization;

namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;

public class SystemTextJsonValidationMetadataProviderTest
{
[Fact]
public void CreateValidationMetadata_SetValidationPropertyName_WithJsonPropertyNameAttribute()
{
var metadataProvider = new SystemTextJsonValidationMetadataProvider();
var propertyName = "sample-data";

var key = ModelMetadataIdentity.ForProperty(typeof(SampleTestClass).GetProperty(nameof(SampleTestClass.NoAttributesProperty)), typeof(int), typeof(SampleTestClass));
var modelAttributes = new ModelAttributes(Array.Empty<object>(), new[] { new JsonPropertyNameAttribute(propertyName) }, Array.Empty<object>());
var context = new ValidationMetadataProviderContext(key, modelAttributes);

// Act
metadataProvider.CreateValidationMetadata(context);

// Assert
Assert.NotNull(context.ValidationMetadata.ValidationModelName);
Assert.Equal(propertyName, context.ValidationMetadata.ValidationModelName);
}

[Fact]
public void CreateValidationMetadata_SetValidationPropertyName_CamelCaseWithDefaultNamingPolicy()
{
var metadataProvider = new SystemTextJsonValidationMetadataProvider();
var propertyName = nameof(SampleTestClass.NoAttributesProperty);

var key = ModelMetadataIdentity.ForProperty(typeof(SampleTestClass).GetProperty(propertyName), typeof(int), typeof(SampleTestClass));
var modelAttributes = new ModelAttributes(Array.Empty<object>(), Array.Empty<object>(), Array.Empty<object>());
var context = new ValidationMetadataProviderContext(key, modelAttributes);

// Act
metadataProvider.CreateValidationMetadata(context);

// Assert
Assert.NotNull(context.ValidationMetadata.ValidationModelName);
Assert.Equal(JsonNamingPolicy.CamelCase.ConvertName(propertyName), context.ValidationMetadata.ValidationModelName);
}

[Theory]
[MemberData(nameof(NamingPolicies))]
public void CreateValidationMetadata_SetValidationPropertyName_WithJsonNamingPolicy(JsonNamingPolicy namingPolicy)
{
var metadataProvider = new SystemTextJsonValidationMetadataProvider(namingPolicy);
var propertyName = nameof(SampleTestClass.NoAttributesProperty);

var key = ModelMetadataIdentity.ForProperty(typeof(SampleTestClass).GetProperty(propertyName), typeof(int), typeof(SampleTestClass));
var modelAttributes = new ModelAttributes(Array.Empty<object>(), Array.Empty<object>(), Array.Empty<object>());
var context = new ValidationMetadataProviderContext(key, modelAttributes);

// Act
metadataProvider.CreateValidationMetadata(context);

// Assert
Assert.NotNull(context.ValidationMetadata.ValidationModelName);
Assert.Equal(namingPolicy.ConvertName(propertyName), context.ValidationMetadata.ValidationModelName);
}

public static TheoryData<JsonNamingPolicy> NamingPolicies
{
get
{
return new TheoryData<JsonNamingPolicy>
{
UpperCaseJsonNamingPolicy.Instance,
JsonNamingPolicy.CamelCase
};
}
}

public class UpperCaseJsonNamingPolicy : System.Text.Json.JsonNamingPolicy
{
public static JsonNamingPolicy Instance = new UpperCaseJsonNamingPolicy();

public override string ConvertName(string name)
{
return name?.ToUpperInvariant();
}
}

public class SampleTestClass
{
public int NoAttributesProperty { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation;

using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;

public class DefaultComplexObjectValidationStrategyTest
{
[Fact]
Expand Down Expand Up @@ -45,6 +47,46 @@ public void GetChildren_ReturnsExpectedElements()
});
}

[Fact]
public void GetChildren_ReturnsExpectedElements_WithValidationModelName()
{
// Arrange
var model = new Person()
{
Age = 23,
Id = 1,
Name = "Joey",
};

var metadata = TestModelMetadataProvider.CreateDefaultProvider(new List<IMetadataDetailsProvider> { new TestValidationModelNameProvider() }).GetMetadataForType(typeof(Person));
var strategy = DefaultComplexObjectValidationStrategy.Instance;

// Act
var enumerator = strategy.GetChildren(metadata, "prefix", model);

// Assert
Assert.Collection(
BufferEntries(enumerator).OrderBy(e => e.Key),
entry =>
{
Assert.Equal("prefix.AGE", entry.Key);
Assert.Equal(23, entry.Model);
Assert.Same(metadata.Properties["Age"], entry.Metadata);
},
entry =>
{
Assert.Equal("prefix.ID", entry.Key);
Assert.Equal(1, entry.Model);
Assert.Same(metadata.Properties["Id"], entry.Metadata);
},
entry =>
{
Assert.Equal("prefix.NAME", entry.Key);
Assert.Equal("Joey", entry.Model);
Assert.Same(metadata.Properties["Name"], entry.Metadata);
});
}

[Fact]
public void GetChildren_SetsModelNull_IfContainerNull()
{
Expand Down Expand Up @@ -149,4 +191,10 @@ public LazyPerson(string input)

public string Name => _string.Substring(3, 5);
}

private class TestValidationModelNameProvider : IValidationMetadataProvider
{
public void CreateValidationMetadata(ValidationMetadataProviderContext context)
=> context.ValidationMetadata.ValidationModelName = context.Key.Name?.ToUpperInvariant();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

namespace Microsoft.AspNetCore.Mvc.NewtonsoftJson;

/// <summary>
/// An implementation of <see cref="IDisplayMetadataProvider"/> and <see cref="IValidationMetadataProvider"/> for
/// the Newtonsoft.Json attribute classes.
/// </summary>
public sealed class NewtonsoftJsonValidationMetadataProvider : IDisplayMetadataProvider, IValidationMetadataProvider
{
private readonly NamingStrategy _jsonNamingPolicy;

/// <summary>
/// Creates a new <see cref="NewtonsoftJsonValidationMetadataProvider"/> with the default <see cref="CamelCaseNamingStrategy"/>
/// </summary>
public NewtonsoftJsonValidationMetadataProvider()
: this(new CamelCaseNamingStrategy())
{ }

/// <summary>
/// Initializes a new instance of <see cref="NewtonsoftJsonValidationMetadataProvider"/> with an optional <see cref="NamingStrategy"/>
/// </summary>
/// <param name="namingStrategy">The <see cref="NamingStrategy"/> to be used to configure the metadata provider.</param>
public NewtonsoftJsonValidationMetadataProvider(NamingStrategy namingStrategy)
{
if (namingStrategy == null)
{
throw new ArgumentNullException(nameof(namingStrategy));
}

_jsonNamingPolicy = namingStrategy;
}

/// <inheritdoc />
public void CreateDisplayMetadata(DisplayMetadataProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

var propertyName = ReadPropertyNameFrom(context.Attributes);

if (!string.IsNullOrEmpty(propertyName))
{
context.DisplayMetadata.DisplayName = () => propertyName;
}
}

/// <inheritdoc />
public void CreateValidationMetadata(ValidationMetadataProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

var propertyName = ReadPropertyNameFrom(context.Attributes);

if (string.IsNullOrEmpty(propertyName))
{
propertyName = _jsonNamingPolicy.GetPropertyName(context.Key.Name!, false);
}

context.ValidationMetadata.ValidationModelName = propertyName!;
}

private static string? ReadPropertyNameFrom(IReadOnlyList<object> attributes)
=> attributes?.OfType<JsonPropertyAttribute>().FirstOrDefault()?.PropertyName;
}
5 changes: 5 additions & 0 deletions src/Mvc/Mvc.NewtonsoftJson/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
#nullable enable
Microsoft.AspNetCore.Mvc.NewtonsoftJson.NewtonsoftJsonValidationMetadataProvider
Microsoft.AspNetCore.Mvc.NewtonsoftJson.NewtonsoftJsonValidationMetadataProvider.CreateDisplayMetadata(Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.DisplayMetadataProviderContext! context) -> void
Microsoft.AspNetCore.Mvc.NewtonsoftJson.NewtonsoftJsonValidationMetadataProvider.CreateValidationMetadata(Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ValidationMetadataProviderContext! context) -> void
Microsoft.AspNetCore.Mvc.NewtonsoftJson.NewtonsoftJsonValidationMetadataProvider.NewtonsoftJsonValidationMetadataProvider() -> void
Microsoft.AspNetCore.Mvc.NewtonsoftJson.NewtonsoftJsonValidationMetadataProvider.NewtonsoftJsonValidationMetadataProvider(Newtonsoft.Json.Serialization.NamingStrategy! namingStrategy) -> void
Loading