Skip to content
Draft
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
93 changes: 93 additions & 0 deletions Easy-Core-Test/Extensions/AttributeExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,97 @@ public void GetTypeAttribute_ReturnsAttributeOrNull()
Assert.NotNull(typeof(SampleType).GetTypeAttribute<DisplayAttribute>());
Assert.Null(typeof(SampleType).GetTypeAttribute<ObsoleteAttribute>());
}

// --- GetTypeAttribute(selector) ---

[Fact]
public void GetTypeAttribute_WithSelector_ReadsValue()
{
Assert.Equal("Sample Display", typeof(SampleType).GetTypeAttribute(d => d.GetName()));
}

[Fact]
public void GetTypeAttribute_WithSelector_ReturnsNullWhenNoAttribute()
{
// AttributeExtensionsTests has no [Display], so selector never runs
Assert.Null(typeof(AttributeExtensionsTests).GetTypeAttribute(d => d.GetName()));
}

// --- GetPropertyAttribute(expression, selector) ---

[Fact]
public void GetPropertyAttribute_ByExpression_WithSelector_ReadsValue()
{
var instance = new SampleType();
Expression<Func<string?>> expression = () => instance.Name;

Assert.Equal("Display Name", expression.GetPropertyAttribute(d => d.GetName()));
}

[Fact]
public void GetPropertyAttribute_ByExpression_WithSelector_ReturnsNullWhenNoAttribute()
{
// Plain has no [Display], so selector never runs
var instance = new SampleType();
Expression<Func<string?>> expression = () => instance.Plain;

Assert.Null(expression.GetPropertyAttribute(d => d.GetName()));
}

// --- GetPropertyAttribute(Type, name, selector) ---

[Fact]
public void GetPropertyAttribute_ByType_WithSelector_ReadsValue()
{
Assert.Equal("Display Name", typeof(SampleType).GetPropertyAttribute(nameof(SampleType.Name), d => d.GetName()));
}

[Fact]
public void GetPropertyAttribute_ByType_WithSelector_ReturnsNullForMissingProperty()
{
Assert.Null(typeof(SampleType).GetPropertyAttribute("Missing", d => d.GetName()));
}

[Fact]
public void GetPropertyAttribute_ByType_WithSelector_ReturnsNullWhenNoAttribute()
{
// Plain exists but has no [Display]
Assert.Null(typeof(SampleType).GetPropertyAttribute(nameof(SampleType.Plain), d => d.GetName()));
}

// --- GetValueAttribute(value, selector) ---

[Fact]
public void GetValueAttribute_ByValue_WithSelector_ReadsValue()
{
Assert.Equal("First Option", SampleEnum.First.GetValueAttribute(d => d.GetName()));
}

[Fact]
public void GetValueAttribute_ByValue_WithSelector_ReturnsNullWhenNoAttribute()
{
// Second has no [Display], so selector never runs
Assert.Null(SampleEnum.Second.GetValueAttribute(d => d.GetName()));
}

// --- GetValueAttribute(Type, memberName, selector) ---

[Fact]
public void GetValueAttribute_ByType_WithSelector_ReadsValue()
{
Assert.Equal("First Option", typeof(SampleEnum).GetValueAttribute("First", d => d.GetName()));
}

[Fact]
public void GetValueAttribute_ByType_WithSelector_ReturnsNullForMissingMember()
{
Assert.Null(typeof(SampleEnum).GetValueAttribute("Missing", d => d.GetName()));
}

[Fact]
public void GetValueAttribute_ByType_WithSelector_ReturnsNullWhenNoAttribute()
{
// Second exists but has no [Display]
Assert.Null(typeof(SampleEnum).GetValueAttribute("Second", d => d.GetName()));
}
}
42 changes: 42 additions & 0 deletions Easy-Core-Test/Extensions/DateAndTimeExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,48 @@ public void ListDaysTo_StepGreaterThanOne()
Assert.Equal(new DateOnly(2024, 1, 9), days[^1]);
}

[Fact]
public void ListDaysTo_DateTime_NegativeStep_IteratesInReverse()
{
var start = new DateTime(2024, 1, 5);
var end = new DateTime(2024, 1, 1);

var days = start.ListDaysTo(end, -1).ToList();

Assert.Equal(5, days.Count);
Assert.Equal(start, days[0]);
Assert.Equal(end, days[^1]);
}

[Fact]
public void ListDaysTo_DateOnly_NegativeStep_IteratesInReverse()
{
var start = new DateOnly(2024, 1, 5);
var end = new DateOnly(2024, 1, 1);

var days = start.ListDaysTo(end, -1).ToList();

Assert.Equal(5, days.Count);
Assert.Equal(start, days[0]);
Assert.Equal(end, days[^1]);
}

[Fact]
public void ListDaysTo_DateTime_ZeroStep_ReturnsEmpty()
{
var days = new DateTime(2024, 1, 1).ListDaysTo(new DateTime(2024, 1, 5), 0).ToList();

Assert.Empty(days);
}

[Fact]
public void ListDaysTo_DateOnly_ZeroStep_ReturnsEmpty()
{
var days = new DateOnly(2024, 1, 1).ListDaysTo(new DateOnly(2024, 1, 5), 0).ToList();

Assert.Empty(days);
}

[Fact]
public void ToCustomString_DefaultSettings_FormatsHoursMinutesSeconds()
{
Expand Down
36 changes: 36 additions & 0 deletions Easy-Core-Test/Extensions/GeneralExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,40 @@ public void TryUpdateModel_CopiesSelectedProperties()
Assert.Equal(1, destination.Id);
Assert.Equal("src", destination.Name);
}

[Fact]
public void DeserializeAnonymousType_PopulatesFields()
{
var json = """{"name":"Alice","age":30}""";
var template = new { name = "", age = 0 };

var result = json.DeserializeAnonymousType(template);

Assert.NotNull(result);
Assert.Equal("Alice", result.name);
Assert.Equal(30, result.age);
}

[Fact]
public void DeserializeAnonymousType_WithOptions_RespectsOptions()
{
// JSON keys are uppercase; options enable case-insensitive matching
var json = """{"NAME":"Bob","AGE":25}""";
var template = new { name = "", age = 0 };
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };

var result = json.DeserializeAnonymousType(template, options);

Assert.NotNull(result);
Assert.Equal("Bob", result.name);
Assert.Equal(25, result.age);
}

[Fact]
public void DeserializeAnonymousType_InvalidJson_ThrowsJsonException()
{
var template = new { name = "" };

Assert.Throws<JsonException>(() => "not valid json".DeserializeAnonymousType(template));
}
}
120 changes: 100 additions & 20 deletions Easy-Core/Extensions/AttributeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,37 +105,83 @@ public static string GetValueDisplayName<TModel>(this TModel value)
return value.GetValueAttribute<TModel, DisplayAttribute>()?.GetName() ?? value?.ToString() ?? "";
}

/// <summary>
/// Returns the matching attribute from the specified type if found.
/// </summary>
/// <typeparam name="TAttribute">The datatype of the attribute to return.</typeparam>
/// <param name="type">The type to check.</param>
public static TAttribute? GetTypeAttribute<TAttribute>(this Type type) where TAttribute : Attribute
/// <summary>
/// Returns the matching attribute from the specified type if found.
/// </summary>
/// <typeparam name="TAttribute">The datatype of the attribute to return.</typeparam>
/// <param name="type">The type to check.</param>
public static TAttribute? GetTypeAttribute<TAttribute>(this Type type) where TAttribute : Attribute
{
return (TAttribute?)Attribute.GetCustomAttribute(type, typeof(TAttribute));
}

/// <summary>
/// Returns the matching attribute from the specified property if found.
/// </summary>
/// <typeparam name="TModel">The datatype of the property to check.</typeparam>
/// <typeparam name="TAttribute">The datatype of the attribute to return.</typeparam>
/// <param name="property">The property to check.</param>
public static TAttribute? GetPropertyAttribute<TModel, TAttribute>(this Expression<Func<TModel>> property) where TAttribute : Attribute
/// <summary>
/// Returns the value of a specified property from the <see cref="DisplayAttribute"/> of a provided type if found.
/// </summary>
/// <param name="type">The type to check.</param>
/// <param name="propertyAccessor">A function to access the desired property from the <see cref="DisplayAttribute"/>.</param>
/// <returns>The value of the specified property from the <see cref="DisplayAttribute"/> if found; otherwise, null.</returns>
public static string? GetTypeAttribute(this Type type, Func<DisplayAttribute, string?> propertyAccessor)
{
DisplayAttribute? displayAttribute = type.GetTypeAttribute<DisplayAttribute>();
return displayAttribute != null ? propertyAccessor(displayAttribute) : null;
}
Comment thread
junc-nf marked this conversation as resolved.

/// <summary>
/// Returns the matching attribute from the specified property if found.
/// </summary>
/// <typeparam name="TModel">The datatype of the property to check.</typeparam>
/// <typeparam name="TAttribute">The datatype of the attribute to return.</typeparam>
/// <param name="property">The property to check.</param>
public static TAttribute? GetPropertyAttribute<TModel, TAttribute>(this Expression<Func<TModel>> property) where TAttribute : Attribute
{
var expression = (MemberExpression)property.Body;
var attribute = expression.Member.GetCustomAttribute(typeof(TAttribute)) as TAttribute;

return attribute;
}

/// <summary>
/// Returns the matching attribute from the specified value if found.
/// </summary>
/// <typeparam name="TModel">The datatype of the property to check.</typeparam>
/// <typeparam name="TAttribute">The datatype of the attribute to return.</typeparam>
/// <param name="value">The value to check.</param>
public static TAttribute? GetValueAttribute<TModel, TAttribute>(this TModel value) where TAttribute : Attribute
/// <summary>
/// Returns the value of a specified property from the <see cref="DisplayAttribute"/> of a provided property if found.
/// </summary>
/// <typeparam name="TModel">The datatype of the property to check.</typeparam>
/// <param name="property">The property to check.</param>
/// <param name="propertyAccessor">A function to access the desired property from the <see cref="DisplayAttribute"/>.</param>
/// <returns>The value of the specified property from the <see cref="DisplayAttribute"/> if found; otherwise, null.</returns>
public static string? GetPropertyAttribute<TModel>(this Expression<Func<TModel>> property, Func<DisplayAttribute, string?> propertyAccessor)
{
var displayAttribute = property.GetPropertyAttribute<TModel, DisplayAttribute>();
return displayAttribute != null ? propertyAccessor(displayAttribute) : null;
}
Comment thread
junc-nf marked this conversation as resolved.

/// <summary>
/// Returns the value of a specified property from the <see cref="DisplayAttribute"/> of a provided property if found.
/// </summary>
/// <param name="type">The type of the parent class of the property.</param>
/// <param name="propertyName">The name of the property in the parent class.</param>
/// <param name="propertyAccessor">A function to access the desired property from the <see cref="DisplayAttribute"/>.</param>
/// <returns>The value of the specified property from the <see cref="DisplayAttribute"/> if found; otherwise, null.</returns>
public static string? GetPropertyAttribute(this Type type, string propertyName, Func<DisplayAttribute, string?> propertyAccessor)
{
var property = type.GetProperty(propertyName);

if (property == null)
return null;

// Get the [Display] attribute from the property (null if not decorated)
var displayAttribute = property.GetCustomAttribute<DisplayAttribute>();

// Let the caller decide what to read from it (Name, Description, etc.)
return displayAttribute != null ? propertyAccessor(displayAttribute) : null;
}
Comment thread
junc-nf marked this conversation as resolved.

/// <summary>
/// Returns the matching attribute from the specified value if found.
/// </summary>
/// <typeparam name="TModel">The datatype of the property to check.</typeparam>
/// <typeparam name="TAttribute">The datatype of the attribute to return.</typeparam>
/// <param name="value">The value to check.</param>
public static TAttribute? GetValueAttribute<TModel, TAttribute>(this TModel value) where TAttribute : Attribute
{
var memberName = value?.ToString();

Expand All @@ -149,4 +195,38 @@ public static string GetValueDisplayName<TModel>(this TModel value)

return attribute;
}

/// <summary>
/// Returns the value of a specified property from the <see cref="DisplayAttribute"/> of a provided property value if found.
/// </summary>
/// <typeparam name="TModel">The type of the property.</typeparam>
/// <param name="value">The value to find the property of.</param>
/// <param name="propertyAccessor">A function to access the desired property from the <see cref="DisplayAttribute"/>.</param>
/// <returns>The value of the specified property from the <see cref="DisplayAttribute"/> if found; otherwise, null.</returns>
public static string? GetValueAttribute<TModel>(this TModel value, Func<DisplayAttribute, string?> propertyAccessor)
{
DisplayAttribute? displayAttribute = value.GetValueAttribute<TModel, DisplayAttribute>();
return displayAttribute != null ? propertyAccessor(displayAttribute) : null;
}
Comment thread
junc-nf marked this conversation as resolved.

/// <summary>
/// Returns the value of a specified property from the <see cref="DisplayAttribute"/> of a provided property if found.
/// </summary>
/// <param name="type">The type of the parent class of the property.</param>
/// <param name="memberName">The name of the member in the parent class.</param>
/// <param name="propertyAccessor">A function to access the desired property from the <see cref="DisplayAttribute"/>.</param>
/// <returns>The value of the specified property from the <see cref="DisplayAttribute"/> if found; otherwise, null.</returns>
public static string? GetValueAttribute(this Type type, string memberName, Func<DisplayAttribute, string?> propertyAccessor)
{
var member = type.GetMember(memberName).FirstOrDefault();

if (member == null)
return null;

var displayAttribute = member.GetCustomAttribute<DisplayAttribute>();

return displayAttribute != null ? propertyAccessor(displayAttribute) : null;
}
Comment thread
junc-nf marked this conversation as resolved.


}
14 changes: 10 additions & 4 deletions Easy-Core/Extensions/DateAndTimeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,10 +239,13 @@ private static DateOnly GetXthDayInMonth(this IEnumerable<DateOnly> days, int xt
/// </summary>
/// <param name="start">The date to start at.</param>
/// <param name="end">The date to end at.</param>
/// <param name="step">The number of days use as an interval.</param>
/// <param name="step">The number of days used as an interval. Use a negative value to iterate in reverse.</param>
public static IEnumerable<DateTime> ListDaysTo(this DateTime start, DateTime end, int step = 1)
{
for (var day = start.Date; day.Date <= end.Date; day = day.AddDays(step))
if (step == 0)
yield break;
Comment on lines +245 to +246

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't seem necessary? Need confirmation from @thirstyape


for (var day = start.Date; step > 0 ? day.Date <= end.Date : day.Date >= end.Date; day = day.AddDays(step))
yield return day;
}

Expand All @@ -251,10 +254,13 @@ public static IEnumerable<DateTime> ListDaysTo(this DateTime start, DateTime end
/// </summary>
/// <param name="start">The date to start at.</param>
/// <param name="end">The date to end at.</param>
/// <param name="step">The number of days use as an interval.</param>
/// <param name="step">The number of days used as an interval. Use a negative value to iterate in reverse.</param>
public static IEnumerable<DateOnly> ListDaysTo(this DateOnly start, DateOnly end, int step = 1)
{
for (var day = start; day <= end; day = day.AddDays(step))
if (step == 0)
yield break;
Comment on lines +260 to +261

for (var day = start; step > 0 ? day <= end : day >= end; day = day.AddDays(step))
yield return day;
}

Expand Down
29 changes: 20 additions & 9 deletions Easy-Core/Extensions/GeneralExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,26 @@ public static JsonSerializerOptions Clone(this JsonSerializerOptions value, bool
return cloned;
}

/// <summary>
/// Creates a lambda expression for the provided property.
/// </summary>
/// <typeparam name="TEntity">The type the property exists within.</typeparam>
/// <param name="propertyName">The name of the property to return.</param>
/// <remarks>
/// Use a dot '.' notation to indicate nested properties (ex. ParentClass.ChildClass.PropertyName).
/// </remarks>
public static Expression<Func<TEntity, object>> ToLambda<TEntity>(this string propertyName)
/// <summary>
/// Deserializes the JSON string into an anonymous type using the provided template.
/// </summary>
/// <typeparam name="T">The type of the anonymous object.</typeparam>
/// <param name="json">The JSON string to deserialize.</param>
/// <param name="_">An instance of the anonymous type to use as a template.</param>
/// <param name="options">Optional JSON serializer options.</param>
/// <returns>The deserialized anonymous object.</returns>
public static T? DeserializeAnonymousType<T>(this string json, T _, JsonSerializerOptions? options = null)
=> JsonSerializer.Deserialize<T>(json, options);
Comment thread
junc-nf marked this conversation as resolved.

/// <summary>
/// Creates a lambda expression for the provided property.
/// </summary>
/// <typeparam name="TEntity">The type the property exists within.</typeparam>
/// <param name="propertyName">The name of the property to return.</param>
/// <remarks>
/// Use a dot '.' notation to indicate nested properties (ex. ParentClass.ChildClass.PropertyName).
/// </remarks>
public static Expression<Func<TEntity, object>> ToLambda<TEntity>(this string propertyName)
{
var parameter = Expression.Parameter(typeof(TEntity));
var current = parameter as Expression;
Expand Down
Loading