Skip to content
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

Support runtime property attributes via TypeDescriptor #53

Merged
Merged
Show file tree
Hide file tree
Changes from 4 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
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="PolySharp" Version="1.13.2" />
<PackageVersion Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.4.0" />
Expand All @@ -13,4 +14,4 @@
<PackageVersion Include="BenchmarkDotNet" Version="0.13.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
</ItemGroup>
</Project>
</Project>
4 changes: 4 additions & 0 deletions src/MiniValidation/MiniValidation.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
<ItemGroup>
<None Include="../../README.md" Pack="true" PackagePath="\" />
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
<PackageReference Include="PolySharp">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>
24 changes: 24 additions & 0 deletions src/MiniValidation/TypeDetailsCache.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;

Expand Down Expand Up @@ -198,6 +200,11 @@ private static (ValidationAttribute[]?, DisplayAttribute?, SkipRecursionAttribut
? paramAttributes.Concat(propertyAttributes)
: propertyAttributes;

if (TryGetAttributesViaTypeDescriptor(property, out var typeDescriptorAttributes))
{
customAttributes = customAttributes.Concat(typeDescriptorAttributes.Cast<Attribute>());
}

foreach (var attr in customAttributes)
{
if (attr is ValidationAttribute validationAttr)
Expand All @@ -218,6 +225,23 @@ private static (ValidationAttribute[]?, DisplayAttribute?, SkipRecursionAttribut
return new(validationAttributes?.ToArray(), displayAttribute, skipRecursionAttribute);
}

private static bool TryGetAttributesViaTypeDescriptor(PropertyInfo property, [NotNullWhen(true)] out IEnumerable<Attribute>? typeDescriptorAttributes)
{
var attributes = TypeDescriptor.GetProperties(property.ReflectedType!)
.Cast<PropertyDescriptor>()
.FirstOrDefault(x => x.Name == property.Name)
?.Attributes;

if (attributes is { Count: > 0 } tdps)
{
typeDescriptorAttributes = tdps.Cast<Attribute>();
return true;
}

typeDescriptorAttributes = null;
return false;
}

private static Type? GetEnumerableType(Type type)
{
if (type.IsInterface && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
Expand Down
5 changes: 5 additions & 0 deletions tests/MiniValidation.UnitTests/TestTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,8 @@ class ClassWithUri
[Required]
public Uri? BaseAddress { get; set; }
}

class TestTypeForTypeDescriptor
{
public string? PropertyToBeRequired { get; set; }
}
16 changes: 15 additions & 1 deletion tests/MiniValidation.UnitTests/TryValidate.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Runtime.CompilerServices;
using Microsoft.Extensions.DependencyInjection;

namespace MiniValidation.UnitTests;
Expand Down Expand Up @@ -396,4 +395,19 @@ public async Task TryValidateAsync_With_ServiceProvider()
Assert.Equal(1, errors.Count);
Assert.Equal(nameof(IServiceProvider), errors.Keys.First());
}

[Fact]
public async Task TryValidateAsync_With_Attribute_Attached_Via_TypeDescriptor()
{
var thingToValidate = new TestTypeForTypeDescriptor();

typeof(TestTypeForTypeDescriptor).AttachAttribute(
nameof(TestTypeForTypeDescriptor.PropertyToBeRequired),
_ => new RequiredAttribute());

var (isValid, errors) = await MiniValidator.TryValidateAsync(thingToValidate);

Assert.False(isValid);
Assert.Equal(1, errors.Count);
}
}
100 changes: 100 additions & 0 deletions tests/MiniValidation.UnitTests/TypeDescriptorExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System.ComponentModel;

namespace MiniValidation.UnitTests
{
internal static class TypeDescriptorExtensions
{
public static void AttachAttribute(this Type type, string propertyName, Func<PropertyDescriptor, Attribute> attributeFactory)
{
var ctd = new PropertyOverridingTypeDescriptor(TypeDescriptor.GetProvider(type).GetTypeDescriptor(type)!);

foreach (PropertyDescriptor pd in TypeDescriptor.GetProperties(type))
{
if (pd.Name == propertyName)
{
var pdWithAttribute = TypeDescriptor.CreateProperty(
type,
pd,
attributeFactory(pd));

ctd.OverrideProperty(pdWithAttribute);
}
}

TypeDescriptor.AddProvider(new TypeDescriptorOverridingProvider(ctd), type);
}
}

// From https://stackoverflow.com/questions/12143650/how-to-add-property-level-attribute-to-the-typedescriptor-at-runtime
internal class PropertyOverridingTypeDescriptor : CustomTypeDescriptor
{
private readonly Dictionary<string, PropertyDescriptor> overridePds = new Dictionary<string, PropertyDescriptor>();

public PropertyOverridingTypeDescriptor(ICustomTypeDescriptor parent)
: base(parent)
{ }

public void OverrideProperty(PropertyDescriptor pd)
{
overridePds[pd.Name] = pd;
}

public override object? GetPropertyOwner(PropertyDescriptor? pd)
{
var propertyOwner = base.GetPropertyOwner(pd);

if (propertyOwner == null)
{
return this;
}

return propertyOwner;
}

public override PropertyDescriptorCollection GetProperties()
{
return GetPropertiesImpl(base.GetProperties());
}

public override PropertyDescriptorCollection GetProperties(Attribute[]? attributes)
{
return GetPropertiesImpl(base.GetProperties(attributes));
}

private PropertyDescriptorCollection GetPropertiesImpl(PropertyDescriptorCollection pdc)
{
var pdl = new List<PropertyDescriptor>(pdc.Count + 1);

foreach (PropertyDescriptor pd in pdc)
{
if (overridePds.ContainsKey(pd.Name))
{
pdl.Add(overridePds[pd.Name]);
}
else
{
pdl.Add(pd);
}
}

var ret = new PropertyDescriptorCollection(pdl.ToArray());

return ret;
}
}

internal class TypeDescriptorOverridingProvider : TypeDescriptionProvider
{
private readonly ICustomTypeDescriptor ctd;

public TypeDescriptorOverridingProvider(ICustomTypeDescriptor ctd)
{
this.ctd = ctd;
}

public override ICustomTypeDescriptor? GetTypeDescriptor(Type objectType, object? instance)
{
return ctd;
}
}
}