Skip to content

Commit 26840cd

Browse files
authored
Merge pull request #48 from ScottLilly/add-uniqueitemsattribute
Add uniqueitemsattribute
2 parents 54afc71 + 8e9ab4a commit 26840cd

File tree

4 files changed

+279
-1
lines changed

4 files changed

+279
-1
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
using System;
2+
using System.Collections;
3+
using System.ComponentModel.DataAnnotations;
4+
using System.Linq;
5+
using System.Reflection;
6+
7+
namespace CSharpExtender.Attributes;
8+
9+
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
10+
public class UniqueItemsAttribute : ValidationAttribute
11+
{
12+
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
13+
{
14+
// If the value is null, let [Required] handle it if needed
15+
if (value == null)
16+
{
17+
return ValidationResult.Success;
18+
}
19+
20+
// Ensure the value is a collection.
21+
// Need to explicitly check for string, as it is enumerable.
22+
if (value is string || value is not IEnumerable collection)
23+
{
24+
return new ValidationResult("The UniqueItemsAttribute must be applied to a collection.");
25+
}
26+
27+
var items = collection.Cast<object>().ToList();
28+
if (items.Count == 0)
29+
{
30+
return ValidationResult.Success;
31+
}
32+
33+
// Check for duplicates based on type
34+
for (int i = 0; i < items.Count - 1; i++)
35+
{
36+
for (int j = i + 1; j < items.Count; j++)
37+
{
38+
if (AreItemsEqual(items[i], items[j]))
39+
{
40+
// Use ErrorMessage if provided; otherwise, use default
41+
string errorMessage = ErrorMessage ??
42+
$"The list contains duplicate items at indices {i} and {j}.";
43+
return new ValidationResult(errorMessage);
44+
}
45+
}
46+
}
47+
48+
return ValidationResult.Success;
49+
}
50+
51+
private bool AreItemsEqual(object item1, object item2)
52+
{
53+
if (item1 == null && item2 == null)
54+
{
55+
return true;
56+
}
57+
if (item1 == null || item2 == null)
58+
{
59+
return false;
60+
}
61+
62+
// Handle simple types (e.g., string, int)
63+
if (item1.GetType().IsValueType || item1 is string)
64+
{
65+
return item1.Equals(item2);
66+
}
67+
68+
// Handle complex objects by comparing all properties
69+
var properties = item1.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
70+
foreach (var prop in properties)
71+
{
72+
var value1 = prop.GetValue(item1);
73+
var value2 = prop.GetValue(item2);
74+
75+
if (value1 == null && value2 == null)
76+
{
77+
continue;
78+
}
79+
if (value1 == null || value2 == null)
80+
{
81+
return false;
82+
}
83+
if (!value1.Equals(value2))
84+
{
85+
return false;
86+
}
87+
}
88+
89+
return true;
90+
}
91+
}

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
Extension methods and classes I often find useful for C# development.
44

5+
## DataAnnotations
6+
7+
Attribute classes to validate properties in models.
8+
9+
- **`UniqueItemsAttribute`**: Check that a collection property does not contain duplicate items.
10+
511
## Collections
612

713
### GenericCache
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
using CSharpExtender.Attributes;
2+
using System.ComponentModel.DataAnnotations;
3+
4+
namespace Test.CSharpExtender.Attributes;
5+
6+
public class UniqueItemsAttributeTests
7+
{
8+
private static ValidationContext GetValidationContext(object instance) => new(instance);
9+
10+
// Test class for complex object validation
11+
private class TestItem(int id, string name)
12+
{
13+
public int Id { get; set; } = id;
14+
public string Name { get; set; } = name;
15+
}
16+
17+
[Fact]
18+
public void UniqueItemsAttribute_NullList_ReturnsSuccess()
19+
{
20+
var model = new { Items = null as List<string> };
21+
var attribute = new UniqueItemsAttribute();
22+
var result = attribute.GetValidationResult(model.Items, GetValidationContext(model));
23+
24+
Assert.Equal(ValidationResult.Success, result);
25+
}
26+
27+
[Fact]
28+
public void UniqueItemsAttribute_EmptyList_ReturnsSuccess()
29+
{
30+
var model = new { Items = new List<string>() };
31+
var attribute = new UniqueItemsAttribute();
32+
var result = attribute.GetValidationResult(model.Items, GetValidationContext(model));
33+
34+
Assert.Equal(ValidationResult.Success, result);
35+
}
36+
37+
[Fact]
38+
public void UniqueItemsAttribute_UniqueStrings_ReturnsSuccess()
39+
{
40+
var model = new { Items = new List<string> { "apple", "banana", "cherry" } };
41+
var attribute = new UniqueItemsAttribute();
42+
var result = attribute.GetValidationResult(model.Items, GetValidationContext(model));
43+
44+
Assert.Equal(ValidationResult.Success, result);
45+
}
46+
47+
[Fact]
48+
public void UniqueItemsAttribute_DuplicateStrings_DefaultErrorMessage()
49+
{
50+
var model = new { Items = new List<string> { "apple", "apple", "cherry" } };
51+
var attribute = new UniqueItemsAttribute();
52+
var result = attribute.GetValidationResult(model.Items, GetValidationContext(model));
53+
54+
Assert.NotEqual(ValidationResult.Success, result);
55+
Assert.Contains("duplicate items", result.ErrorMessage);
56+
Assert.Contains("indices 0 and 1", result.ErrorMessage);
57+
}
58+
59+
[Fact]
60+
public void UniqueItemsAttribute_DuplicateStrings_CustomErrorMessage()
61+
{
62+
var model = new { Items = new List<string> { "apple", "apple", "cherry" } };
63+
var attribute = new UniqueItemsAttribute { ErrorMessage = "All items must be unique in the list." };
64+
var result = attribute.GetValidationResult(model.Items, GetValidationContext(model));
65+
66+
Assert.NotEqual(ValidationResult.Success, result);
67+
Assert.Equal("All items must be unique in the list.", result.ErrorMessage);
68+
}
69+
70+
[Fact]
71+
public void UniqueItemsAttribute_UniqueNumbers_ReturnsSuccess()
72+
{
73+
var model = new { Items = new List<int> { 1, 2, 3 } };
74+
var attribute = new UniqueItemsAttribute();
75+
var result = attribute.GetValidationResult(model.Items, GetValidationContext(model));
76+
77+
Assert.Equal(ValidationResult.Success, result);
78+
}
79+
80+
[Fact]
81+
public void UniqueItemsAttribute_DuplicateNumbers_DefaultErrorMessage()
82+
{
83+
var model = new { Items = new List<int> { 1, 2, 2 } };
84+
var attribute = new UniqueItemsAttribute();
85+
var result = attribute.GetValidationResult(model.Items, GetValidationContext(model));
86+
87+
Assert.NotEqual(ValidationResult.Success, result);
88+
Assert.Contains("duplicate items", result.ErrorMessage);
89+
Assert.Contains("indices 1 and 2", result.ErrorMessage);
90+
}
91+
92+
[Fact]
93+
public void UniqueItemsAttribute_DuplicateNumbers_CustomErrorMessage()
94+
{
95+
var model = new { Items = new List<int> { 1, 2, 2 } };
96+
var attribute = new UniqueItemsAttribute { ErrorMessage = "Numbers must be unique." };
97+
var result = attribute.GetValidationResult(model.Items, GetValidationContext(model));
98+
99+
Assert.NotEqual(ValidationResult.Success, result);
100+
Assert.Equal("Numbers must be unique.", result.ErrorMessage);
101+
}
102+
103+
[Fact]
104+
public void UniqueItemsAttribute_UniqueComplexObjects_ReturnsSuccess()
105+
{
106+
var model = new
107+
{
108+
Items = new List<TestItem>
109+
{
110+
new TestItem(1, "apple"),
111+
new TestItem(2, "banana")
112+
}
113+
};
114+
115+
var attribute = new UniqueItemsAttribute();
116+
var result = attribute.GetValidationResult(model.Items, GetValidationContext(model));
117+
118+
Assert.Equal(ValidationResult.Success, result);
119+
}
120+
121+
[Fact]
122+
public void UniqueItemsAttribute_DuplicateComplexObjects_DefaultErrorMessage()
123+
{
124+
var model = new
125+
{
126+
Items = new List<TestItem>
127+
{
128+
new TestItem(1, "apple"),
129+
new TestItem(1, "apple")
130+
}
131+
};
132+
133+
var attribute = new UniqueItemsAttribute();
134+
var result = attribute.GetValidationResult(model.Items, GetValidationContext(model));
135+
136+
Assert.NotEqual(ValidationResult.Success, result);
137+
Assert.Contains("duplicate items", result.ErrorMessage);
138+
Assert.Contains("indices 0 and 1", result.ErrorMessage);
139+
}
140+
141+
[Fact]
142+
public void UniqueItemsAttribute_DuplicateComplexObjects_CustomErrorMessage()
143+
{
144+
var model = new
145+
{
146+
Items = new List<TestItem>
147+
{
148+
new TestItem(1, "apple"),
149+
new TestItem(1, "apple")
150+
}
151+
};
152+
153+
var attribute = new UniqueItemsAttribute { ErrorMessage = "Objects in the list must have unique values." };
154+
var result = attribute.GetValidationResult(model.Items, GetValidationContext(model));
155+
156+
Assert.NotEqual(ValidationResult.Success, result);
157+
Assert.Equal("Objects in the list must have unique values.", result.ErrorMessage);
158+
}
159+
160+
[Fact]
161+
public void UniqueItemsAttribute_SingleString_ReturnsError()
162+
{
163+
var model = new { Items = "not a list" };
164+
var attribute = new UniqueItemsAttribute();
165+
var result = attribute.GetValidationResult(model.Items, GetValidationContext(model));
166+
167+
Assert.NotEqual(ValidationResult.Success, result);
168+
Assert.Contains("must be applied to a collection", result.ErrorMessage);
169+
}
170+
171+
[Fact]
172+
public void UniqueItemsAttribute_NonCollectionNonString_ReturnsError()
173+
{
174+
var model = new { Items = 42 }; // An int, not a string or collection
175+
var attribute = new UniqueItemsAttribute();
176+
var result = attribute.GetValidationResult(model.Items, GetValidationContext(model));
177+
178+
Assert.NotEqual(ValidationResult.Success, result);
179+
Assert.Contains("must be applied to a collection", result.ErrorMessage);
180+
}
181+
}

Test.CSharpExtender/Test.CSharpExtender.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>net8.0</TargetFramework>

0 commit comments

Comments
 (0)