diff --git a/Easy-Core-Test/Extensions/AttributeExtensionsTests.cs b/Easy-Core-Test/Extensions/AttributeExtensionsTests.cs index 2dde42e..33d933c 100644 --- a/Easy-Core-Test/Extensions/AttributeExtensionsTests.cs +++ b/Easy-Core-Test/Extensions/AttributeExtensionsTests.cs @@ -108,4 +108,97 @@ public void GetTypeAttribute_ReturnsAttributeOrNull() Assert.NotNull(typeof(SampleType).GetTypeAttribute()); Assert.Null(typeof(SampleType).GetTypeAttribute()); } + + // --- 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> 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> 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())); + } } diff --git a/Easy-Core-Test/Extensions/DateAndTimeExtensionsTests.cs b/Easy-Core-Test/Extensions/DateAndTimeExtensionsTests.cs index a2c519a..9a37050 100644 --- a/Easy-Core-Test/Extensions/DateAndTimeExtensionsTests.cs +++ b/Easy-Core-Test/Extensions/DateAndTimeExtensionsTests.cs @@ -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() { diff --git a/Easy-Core-Test/Extensions/GeneralExtensionsTests.cs b/Easy-Core-Test/Extensions/GeneralExtensionsTests.cs index ba41b55..1695b89 100644 --- a/Easy-Core-Test/Extensions/GeneralExtensionsTests.cs +++ b/Easy-Core-Test/Extensions/GeneralExtensionsTests.cs @@ -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(() => "not valid json".DeserializeAnonymousType(template)); + } } diff --git a/Easy-Core/Extensions/AttributeExtensions.cs b/Easy-Core/Extensions/AttributeExtensions.cs index 3189379..4195762 100644 --- a/Easy-Core/Extensions/AttributeExtensions.cs +++ b/Easy-Core/Extensions/AttributeExtensions.cs @@ -105,23 +105,35 @@ public static string GetValueDisplayName(this TModel value) return value.GetValueAttribute()?.GetName() ?? value?.ToString() ?? ""; } - /// - /// Returns the matching attribute from the specified type if found. - /// - /// The datatype of the attribute to return. - /// The type to check. - public static TAttribute? GetTypeAttribute(this Type type) where TAttribute : Attribute + /// + /// Returns the matching attribute from the specified type if found. + /// + /// The datatype of the attribute to return. + /// The type to check. + public static TAttribute? GetTypeAttribute(this Type type) where TAttribute : Attribute { return (TAttribute?)Attribute.GetCustomAttribute(type, typeof(TAttribute)); } - /// - /// Returns the matching attribute from the specified property if found. - /// - /// The datatype of the property to check. - /// The datatype of the attribute to return. - /// The property to check. - public static TAttribute? GetPropertyAttribute(this Expression> property) where TAttribute : Attribute + /// + /// Returns the value of a specified property from the of a provided type if found. + /// + /// The type to check. + /// A function to access the desired property from the . + /// The value of the specified property from the if found; otherwise, null. + public static string? GetTypeAttribute(this Type type, Func propertyAccessor) + { + DisplayAttribute? displayAttribute = type.GetTypeAttribute(); + return displayAttribute != null ? propertyAccessor(displayAttribute) : null; + } + + /// + /// Returns the matching attribute from the specified property if found. + /// + /// The datatype of the property to check. + /// The datatype of the attribute to return. + /// The property to check. + public static TAttribute? GetPropertyAttribute(this Expression> property) where TAttribute : Attribute { var expression = (MemberExpression)property.Body; var attribute = expression.Member.GetCustomAttribute(typeof(TAttribute)) as TAttribute; @@ -129,13 +141,47 @@ public static string GetValueDisplayName(this TModel value) return attribute; } - /// - /// Returns the matching attribute from the specified value if found. - /// - /// The datatype of the property to check. - /// The datatype of the attribute to return. - /// The value to check. - public static TAttribute? GetValueAttribute(this TModel value) where TAttribute : Attribute + /// + /// Returns the value of a specified property from the of a provided property if found. + /// + /// The datatype of the property to check. + /// The property to check. + /// A function to access the desired property from the . + /// The value of the specified property from the if found; otherwise, null. + public static string? GetPropertyAttribute(this Expression> property, Func propertyAccessor) + { + var displayAttribute = property.GetPropertyAttribute(); + return displayAttribute != null ? propertyAccessor(displayAttribute) : null; + } + + /// + /// Returns the value of a specified property from the of a provided property if found. + /// + /// The type of the parent class of the property. + /// The name of the property in the parent class. + /// A function to access the desired property from the . + /// The value of the specified property from the if found; otherwise, null. + public static string? GetPropertyAttribute(this Type type, string propertyName, Func 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(); + + // Let the caller decide what to read from it (Name, Description, etc.) + return displayAttribute != null ? propertyAccessor(displayAttribute) : null; + } + + /// + /// Returns the matching attribute from the specified value if found. + /// + /// The datatype of the property to check. + /// The datatype of the attribute to return. + /// The value to check. + public static TAttribute? GetValueAttribute(this TModel value) where TAttribute : Attribute { var memberName = value?.ToString(); @@ -149,4 +195,38 @@ public static string GetValueDisplayName(this TModel value) return attribute; } + + /// + /// Returns the value of a specified property from the of a provided property value if found. + /// + /// The type of the property. + /// The value to find the property of. + /// A function to access the desired property from the . + /// The value of the specified property from the if found; otherwise, null. + public static string? GetValueAttribute(this TModel value, Func propertyAccessor) + { + DisplayAttribute? displayAttribute = value.GetValueAttribute(); + return displayAttribute != null ? propertyAccessor(displayAttribute) : null; + } + + /// + /// Returns the value of a specified property from the of a provided property if found. + /// + /// The type of the parent class of the property. + /// The name of the member in the parent class. + /// A function to access the desired property from the . + /// The value of the specified property from the if found; otherwise, null. + public static string? GetValueAttribute(this Type type, string memberName, Func propertyAccessor) + { + var member = type.GetMember(memberName).FirstOrDefault(); + + if (member == null) + return null; + + var displayAttribute = member.GetCustomAttribute(); + + return displayAttribute != null ? propertyAccessor(displayAttribute) : null; + } + + } diff --git a/Easy-Core/Extensions/DateAndTimeExtensions.cs b/Easy-Core/Extensions/DateAndTimeExtensions.cs index 2413f07..d3dfff5 100644 --- a/Easy-Core/Extensions/DateAndTimeExtensions.cs +++ b/Easy-Core/Extensions/DateAndTimeExtensions.cs @@ -239,10 +239,13 @@ private static DateOnly GetXthDayInMonth(this IEnumerable days, int xt /// /// The date to start at. /// The date to end at. - /// The number of days use as an interval. + /// The number of days used as an interval. Use a negative value to iterate in reverse. public static IEnumerable 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; + + for (var day = start.Date; step > 0 ? day.Date <= end.Date : day.Date >= end.Date; day = day.AddDays(step)) yield return day; } @@ -251,10 +254,13 @@ public static IEnumerable ListDaysTo(this DateTime start, DateTime end /// /// The date to start at. /// The date to end at. - /// The number of days use as an interval. + /// The number of days used as an interval. Use a negative value to iterate in reverse. public static IEnumerable ListDaysTo(this DateOnly start, DateOnly end, int step = 1) { - for (var day = start; day <= end; day = day.AddDays(step)) + if (step == 0) + yield break; + + for (var day = start; step > 0 ? day <= end : day >= end; day = day.AddDays(step)) yield return day; } diff --git a/Easy-Core/Extensions/GeneralExtensions.cs b/Easy-Core/Extensions/GeneralExtensions.cs index e6393b2..0750dda 100644 --- a/Easy-Core/Extensions/GeneralExtensions.cs +++ b/Easy-Core/Extensions/GeneralExtensions.cs @@ -60,15 +60,26 @@ public static JsonSerializerOptions Clone(this JsonSerializerOptions value, bool return cloned; } - /// - /// Creates a lambda expression for the provided property. - /// - /// The type the property exists within. - /// The name of the property to return. - /// - /// Use a dot '.' notation to indicate nested properties (ex. ParentClass.ChildClass.PropertyName). - /// - public static Expression> ToLambda(this string propertyName) + /// + /// Deserializes the JSON string into an anonymous type using the provided template. + /// + /// The type of the anonymous object. + /// The JSON string to deserialize. + /// An instance of the anonymous type to use as a template. + /// Optional JSON serializer options. + /// The deserialized anonymous object. + public static T? DeserializeAnonymousType(this string json, T _, JsonSerializerOptions? options = null) + => JsonSerializer.Deserialize(json, options); + + /// + /// Creates a lambda expression for the provided property. + /// + /// The type the property exists within. + /// The name of the property to return. + /// + /// Use a dot '.' notation to indicate nested properties (ex. ParentClass.ChildClass.PropertyName). + /// + public static Expression> ToLambda(this string propertyName) { var parameter = Expression.Parameter(typeof(TEntity)); var current = parameter as Expression;