generated from kasthack-labs/dotnet-repo-template
-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
1.4.0: add ValueTuple support, add an option to skip get-only compute…
…d properties, tighten compiler warnings, update dotnet workflow
- Loading branch information
Showing
34 changed files
with
597 additions
and
291 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,36 +1,41 @@ | ||
namespace kasthack.NotEmpty.Core | ||
namespace kasthack.NotEmpty.Core; | ||
|
||
public class AssertOptions | ||
{ | ||
public class AssertOptions | ||
{ | ||
/// <summary> | ||
/// Allow zeros in number arrays. Useful when you have binary data as a byte array. | ||
/// </summary> | ||
public bool AllowZerosInNumberArrays { get; set; } = false; | ||
|
||
/// <summary> | ||
/// Maximum assert depth. Useful for preventing stack overflows for objects with generated properties / complex graphs. | ||
/// </summary> | ||
public int? MaxDepth { get; set; } = 100; | ||
|
||
/// <summary> | ||
/// Allows empty strings but not nulls. | ||
/// </summary> | ||
public bool AllowEmptyStrings { get; set; } = false; | ||
|
||
/// <summary> | ||
/// Allows empty strings but not nulls. | ||
/// </summary> | ||
public bool AllowEmptyCollections { get; set; } = false; | ||
|
||
/// <summary> | ||
/// Allows bool properties to be false. | ||
/// </summary> | ||
public bool AllowFalseBooleanProperties { get; set; } = false; | ||
|
||
/// <summary> | ||
/// Allows enum values to have default values if there's a defined option for that. | ||
/// </summary> | ||
public bool AllowDefinedDefaultEnumValues { get; set; } = false; | ||
internal static AssertOptions Default { get; } = new(); | ||
} | ||
/// <summary> | ||
/// Allow zeros in number arrays. Useful when you have binary data as a byte array. | ||
/// </summary> | ||
public bool AllowZerosInNumberArrays { get; set; } = false; | ||
|
||
/// <summary> | ||
/// Maximum assert depth. Useful for preventing stack overflows for objects with generated properties / complex graphs. | ||
/// </summary> | ||
public int? MaxDepth { get; set; } = 100; | ||
|
||
/// <summary> | ||
/// Allows empty strings but not nulls. | ||
/// </summary> | ||
public bool AllowEmptyStrings { get; set; } = false; | ||
|
||
/// <summary> | ||
/// Allows empty strings but not nulls. | ||
/// </summary> | ||
public bool AllowEmptyCollections { get; set; } = false; | ||
|
||
/// <summary> | ||
/// Allows bool properties to be false. | ||
/// </summary> | ||
public bool AllowFalseBooleanProperties { get; set; } = false; | ||
|
||
/// <summary> | ||
/// Allows enum values to have default values if there's a defined option for that. | ||
/// </summary> | ||
public bool AllowDefinedDefaultEnumValues { get; set; } = false; | ||
|
||
/// <summary> | ||
/// Ignore computed properties. | ||
/// </summary> | ||
public bool IgnoreComputedProperties { get; set; } = false; | ||
|
||
internal static AssertOptions Default { get; } = new(); | ||
} |
104 changes: 90 additions & 14 deletions
104
src/kasthack.NotEmpty.Core/CachedPropertyExtractor{T}.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,27 +1,103 @@ | ||
namespace kasthack.NotEmpty.Core | ||
namespace kasthack.NotEmpty.Core; | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Linq.Expressions; | ||
using System.Reflection; | ||
using System.Runtime.CompilerServices; | ||
|
||
// Returns all properties as an array of KV pairs | ||
internal static class CachedPropertyExtractor<T> | ||
{ | ||
using System; | ||
using System.Reflection; | ||
private static IReadOnlyList<(string Name, bool Computed, Func<T, object?> Getter)> Properties { get; } = GetPropertyAccessors(); | ||
|
||
public static PathValue[] GetPropertiesAndTupleFields(T value) | ||
{ | ||
if (Properties.Count == 0) | ||
{ | ||
return Array.Empty<PathValue>(); | ||
} | ||
|
||
// Returns all properties as an array of KV pairs | ||
internal static class CachedPropertyExtractor<T> | ||
var props = new PathValue[Properties.Count]; | ||
for (int i = 0; i < props.Length; i++) | ||
{ | ||
var (name, computed, getter) = Properties[i]; | ||
props[i] = new PathValue(name, computed, getter(value!)); | ||
} | ||
|
||
return props; | ||
} | ||
|
||
internal static IReadOnlyList<(string Name, bool Computed, Func<T, object?> Getter)> GetPropertyAccessors() | ||
{ | ||
private static readonly PropertyInfo[] Properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty); | ||
// https://stackoverflow.com/questions/2210309/how-to-find-out-if-a-property-is-an-auto-implemented-property-with-reflection | ||
var type = typeof(T); | ||
|
||
var fieldnames = type | ||
.GetFields(BindingFlags.NonPublic | BindingFlags.Instance) | ||
.Select(a => a.Name) | ||
.ToHashSet(); | ||
var allProperties = type | ||
.GetProperties(BindingFlags.Instance | BindingFlags.Public); | ||
|
||
public static PathValue[] GetProperties(T? value) | ||
var propertyAccessors = allProperties | ||
.Where(property => property.CanRead && property.GetMethod!.IsPublic) | ||
.Select<PropertyInfo, (string Name, bool Computed, Func<Expression, MemberExpression> ExpressionBuilder)>( | ||
propertyInfo => (propertyInfo.Name, IsComputed(propertyInfo), instance => Expression.Property(instance, propertyInfo))) | ||
.ToList(); | ||
|
||
var fieldAccessors = type.IsValueTuple() | ||
? type | ||
.GetFields(BindingFlags.Public | BindingFlags.Instance) | ||
.Select<FieldInfo, (string Name, bool Computed, Func<Expression, MemberExpression> ExpressionBuilder)>( | ||
fieldInfo => (fieldInfo.Name, false, instance => Expression.Field(instance, fieldInfo))) | ||
.ToArray() | ||
: Array.Empty<(string Name, bool Computed, Func<Expression, MemberExpression> ExpressionBuilder)>(); | ||
|
||
return propertyAccessors | ||
.Concat(fieldAccessors) | ||
.Select(accessorPair => | ||
{ | ||
var instance = Expression.Parameter(typeof(T), "instance"); | ||
var memberAccessor = accessorPair.ExpressionBuilder(instance); | ||
var convert = Expression.Convert(memberAccessor, typeof(object)); | ||
var lambda = Expression.Lambda<Func<T, object?>>(convert, instance); | ||
return (accessorPair.Name, accessorPair.Computed, lambda.Compile()); | ||
}).ToArray(); | ||
|
||
bool IsComputed(PropertyInfo property) | ||
{ | ||
if (Properties.Length == 0) | ||
// has a setter / init | ||
if (property.CanWrite) | ||
{ | ||
return false; | ||
} | ||
|
||
// get-only auto-property | ||
var isGetOnlyAutoProperty = | ||
property.GetMethod!.CustomAttributes.Any(a => a.AttributeType == typeof(CompilerGeneratedAttribute)) | ||
&& fieldnames.Contains($"<{property.Name}>k__BackingField"); | ||
|
||
if (isGetOnlyAutoProperty) | ||
{ | ||
return false; | ||
} | ||
|
||
// init-only property in anonymous type | ||
if (type.IsAnonymousType()) | ||
{ | ||
return Array.Empty<PathValue>(); | ||
return false; | ||
} | ||
|
||
var props = new PathValue[Properties.Length]; | ||
for (int i = 0; i < props.Length; i++) | ||
// built-in tuples | ||
if (type.IsTuple()) | ||
{ | ||
props[i] = new PathValue(Properties[i].Name, Properties[i].GetValue(value)); | ||
return false; | ||
} | ||
|
||
return props; | ||
return true; | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,4 @@ | ||
namespace kasthack.NotEmpty.Core | ||
{ | ||
internal readonly struct PathValue | ||
{ | ||
public PathValue(string path, object? value) | ||
{ | ||
this.Path = path; | ||
this.Value = value; | ||
} | ||
|
||
public readonly string Path { get; } | ||
|
||
public readonly object? Value { get; } | ||
} | ||
internal readonly record struct PathValue(string Path, bool IsComputed, object? Value); | ||
} |
Oops, something went wrong.