Automapper - Get all entries in resolver - c#

I'm working on a project heavily relying on Automapper, and most of the times we are mapping complete sets of data into a set view models, for example
IEnumerable<ObjectA> ListOfObjectA = MockupOfObjectA;
IEnumerable<ViewModelA> = Mapper.Map<IEnumerable<ObjectA>>(ListOfOjectA)
In the mapping setup we are using Custom resolvers thanks to IMemberValueResolver. The parameters and accessible data in a Resolve and ResolveStatic-method, is only the current entity being mapped. Is it possible to access the complete source (ListOfOjectA) in this case, inside the resolver?
So far I am adding the ListOfOjectA into MappingOperationsOptions.Items and use them from context.Items, but this is a work around that is not easy to work with and does not scale well.
I hope I made my question relatively clear.

It's worth pointing out that you're not really mapping ObjectA to ViewModelA. Rather (ObjectA, List<ObjectA>) to ViewModelA, as you can't seem to define ViewModelA without List<ObjectA>.
To simulate, say ObjectA has an Index property as well as the number of Pages it contains.
public class ObjectA
{
public int Index { get; set; }
public int Pages { get; set; }
public string MyProperty { get; set; }
}
And for ViewModelA we want to resolve StartPage, based on the properties of the previous ObjectA's.
public class ViewModelA
{
public int StartPage { get; set; }
public string MyProperty { get; set; }
}
We can clean up your current approach using extension methods.
public static class AutoMapperExt
{
public static TDestination MapWithSource<TSource, TDestination>(this IMapper mapper, TSource source)
=> mapper.Map<TSource, TDestination>(source, opts => opts.Items[typeof(TSource).ToString()] = source);
public static TSource GetSource<TSource>(this ResolutionContext context)
=> (TSource)context.Items[typeof(TSource).ToString()];
}
Using these methods we no longer need to handle the context's Items collection directly.
class Program
{
static void Main(string[] args)
{
var config =
new MapperConfiguration(cfg =>
cfg.CreateMap<ObjectA, ViewModelA>()
.ForMember(dest => dest.StartPage, opt => opt.MapFrom<CustomResolver, int>(src => src.Index))
);
var mapper = config.CreateMapper();
var source = new List<ObjectA>
{
new ObjectA { Index = 0, Pages = 3, MyProperty = "Foo" },
new ObjectA { Index = 1, Pages = 2, MyProperty = "Bar" },
new ObjectA { Index = 2, Pages = 1, MyProperty = "Foz" },
};
var result = mapper.MapWithSource<List<ObjectA>, List<ViewModelA>>(source);
result.ForEach(o => Console.WriteLine(o.StartPage)); // prints 1,4,6
Console.ReadKey();
}
}
public class CustomResolver : IMemberValueResolver<object, object, int, int>
{
public int Resolve(object source, object destination, int sourceMember, int destMember, ResolutionContext context)
{
var index = sourceMember;
var list = context.GetSource<List<ObjectA>>();
var pages = 1;
for (int i = 0; i < index; i++)
{
pages += list[i].Pages;
}
return pages;
}
}
If you want to reuse CustomResolver on different classes, you can abstract the properties it operates on into an interface.
public interface IHavePages
{
int Index { get; }
int Pages { get; }
}
public class ObjectA : IHavePages
{
public int Index { get; set; }
public int Pages { get; set; }
public string MyProperty { get; set; }
}
This way the resolver is no longer bound to a concrete implementation. We can now even use the interface as a type parameter.
public class CustomResolver : IMemberValueResolver<IHavePages, object, int, int>
{
public int Resolve(IHavePages source, object destination, int sourceMember, int destMember, ResolutionContext context)
{
var hasPages = source;
var index = sourceMember;
var list = context.GetSource<List<IHavePages>>();
var pages = 1;
for (int i = 0; i < index; i++)
{
pages += list[i].Pages;
}
return pages;
}
}
All we need to do is transform our List<ObjectA> before mapping.
var listOfObjectA = new List<ObjectA>
{
new ObjectA { Index = 0, Pages = 3, MyProperty = "Foo" },
new ObjectA { Index = 1, Pages = 2, MyProperty = "Bar" },
new ObjectA { Index = 2, Pages = 1, MyProperty = "Foz" },
};
var source = listOfObjectA.OfType<IHavePages>().ToList();
var result = mapper.MapWithSource<List<IHavePages>, List<ViewModelA>>(source);
// AutoMapper still maps properties that aren't part of the interface
result.ForEach(o => Console.WriteLine($"{o.StartPage} - {o.MyProperty}"));
Once you code to an interface, the sourceMember in the CustomResolver becomes redundant. We can now get it through the passed source. Allowing for one final refactor, as we derive from IValueResolver instead of IMemberValueResolver.
public class CustomResolver : IValueResolver<IHavePages, object, int>
{
public int Resolve(IHavePages source, object destination, int destMember, ResolutionContext context)
{
var list = context.GetSource<List<IHavePages>>();
var pages = 1;
for (int i = 0; i < source.Index; i++)
{
pages += list[i].Pages;
}
return pages;
}
}
Updating the signature.
cfg.CreateMap<ObjectA, ViewModelA>()
.ForMember(dest => dest.StartPage, opt => opt.MapFrom<CustomResolver>())
How far you take it is entirely up to you, but you can improve code reuse by introducing abstractions.

You can map a collection of items from a dto collection or other class in a way under.
public Order Convert(OrderDto orderDto)
{
var order = new Order { OrderLines = new OrderLines() };
order.OrderLines = Mapper.Map<List<OrderLine>>(orderDto.Positions);
return order;
}
And your profile class constructor can be written in a way. This is a only a example. You do not need to accept a list in your resolver, you can do it for one object and map to list from outside.
public Profile()
{
CreateMap<PositionDto, OrderLine>()
.ForMember(dest => dest, opt => opt.ResolveUsing<OrderResolver>());
}
}
}

If you prefer not to use the ResolutionContext, you can set up a mapping via an intermediate object holding both the current source item as also the full source list.
Use a lightweight value type, eg. a Tuple or ValueTuple.
The mapping below uses a ValueTuple (but can also be expressed using a Tuple).
Note that the intent and prerequisites of this mapping are quite explicit; it indicates that 2 input/source elements are required: ObjectA and IEnumerable<ObjectA> (passed via a ValueTuple).
Mapper.Initialize(cfg =>
cfg.CreateMap<(ObjectA, IEnumerable<ObjectA>), ViewModelA>()
.ForMember(
dest => dest.Name,
opt => opt.MapFrom<CustomResolver>()
));
At the time of mapping, you project the source list into one of corresponding ValueTuples.
Prefer to keep the flow streaming using only 1 current ValueTuple.
var viewModels =
Mapper.Map<IEnumerable<ViewModelA>>(
ListOfObjectA.Select(o => (o, ListOfObjectA))
);
A custom IValueResolver receives both the current input item and the full list via the ValueTuple source argument.
public class CustomResolver :
IValueResolver<
(ObjectA Item, IEnumerable<ObjectA> List),
ViewModelA,
String
>
{
public string Resolve(
(ObjectA Item, IEnumerable<ObjectA> List) source,
ViewModelA destination,
string destMember,
ResolutionContext context
)
{
/* Retrieve something via the list. */
var suffix = source.List.Count().ToString();
return $"{source.Item.Name} {suffix}";
}
}
Full example.
IEnumerable<ObjectA> ListOfObjectA = new List<ObjectA> {
new ObjectA { Name = "One" },
new ObjectA { Name = "Two" },
new ObjectA { Name = "Three" }
};
Mapper.Initialize(cfg =>
cfg.CreateMap<(ObjectA, IEnumerable<ObjectA>), ViewModelA>()
.ForMember(
dest => dest.Name,
opt => opt.MapFrom<CustomResolver>()
));
var viewModels =
Mapper.Map<IEnumerable<ViewModelA>>(
ListOfObjectA.Select(o => (o, ListOfObjectA))
);

Related

Get the object instance from a LambdaExpression

I want to pass an instance of a class to a method using an Expression<T> and retrieve the instance from within the method:
public class MyOb
{
public void Receive(Expression<Func<Item>> item)
{
// Here I would like to get item as Item
// Have tried various things such as
var x = ((MemberExpression)(item.Body)).Member;
int y = x.IntProp // should be 123.
}
}
public class Item
{
public int IntProp { get; set; } = 123;
}
MyOb mo = new();
Item myItem = new();
mo.Receive(() => myItem);
That would not be that easy cause compiler will generate a special class to handle closure which will store the value of the local variable (myItem). Something like this should do the trick:
public static void Receive(Expression<Func<Item>> item)
{
if (item.Body is MemberExpression { Expression: ConstantExpression constE })
{
var itemValue = constE.Type.GetFields()
.Where(fi => fi.FieldType.IsAssignableTo(typeof(Item)))
.Single() // possibly proper validation message if multiple found
.GetValue(constE.Value);
var intProp = ((Item)itemValue).IntProp; // your value
Console.WriteLine(intProp); // will print 123 for your code
}
}

Automapper - Access default map function in MapFrom to fallback

I'm writing an extension method in order to do translation with Automapper.
I have some classes :
public class TranslatableClass : ITranslatable<TranslationClass>
{
public string Id { get; set; }
public string Label { get; set; }
public string Description { get; set; }
public List<TranslationClass> Translations { get; set; }
public string OtherEntityId { get; set; }
public string OtherEntityLabel { get; set; }
public List<OtherEntityTranslation> OtherEntityTranslations { get; set; }
}
public class TranslationClass : ITranslation
{
public Guid LanguageId { get; set; }
public string Label { get; set; }
public string Description { get; set; }
}
public class TranslatedClass
{
public string Id { get; set; }
public string Label { get; set; }
public string Description { get; set; }
public string OtherEntityLabel { get; set; }
}
public class OtherEntityTranslation : ITranslation
{
public string Label { get; set; }
public Guid LanguageId { get; set; }
}
I'd like to get an extension method like this one :
cfg.CreateMap<TranslatableClass, TranslatedClass>()
.ForMember(t => t.OtherEntityLabel, opt => opt.MapFromTranslation(t => t.OtherEntityTranslations, oet => oet.Label));
And my extension method looks like this one
public static void MapFromTranslation<TSource, TDestination, TMember, TTranslation>(this IMemberConfigurationExpression<TSource, TDestination, TMember> opt, Func<TSource, IEnumerable<TTranslation>> getTranslations, Func<TTranslation, string> getValue)
where TTranslation : ITranslation
{
opt.MapFrom((src, _, _, context) =>
{
string result = null; // here is the pain point ; I'd like to get the value as if I was automapper
if (context.Options.Items.TryGetValue(LANGUAGE, out object contextLanguage) && contextLanguage is Guid languageId)
{
var translations = getTranslations(src);
var translation = translations.FirstOrDefault(t => t.LanguageId == languageId);
if (translation != null)
{
result = getValue(translation);
}
}
return result;
});
}
The issue I'm facing is I can't find a nice way to get the default behavior of AutoMapper when I don't have a translation. In this implementation, if I don't find a translation for my language, the value will be null while it should be the value of the source object (which is the default value).
I try to put PreCondition before the MapFrom but that doesn't map the property so I get null too.
I can try to get the value from the source object with reflexion but I will lose all the capabilities of Automapper like naming convention and other stuffs.
public static void MapFromTranslation<TSource, TDestination, TMember, TTranslation>(this IMemberConfigurationExpression<TSource, TDestination, TMember> opt, Func<TSource, IEnumerable<TTranslation>> getTranslations, Func<TTranslation, string> getValue)
where TTranslation : ITranslation
{
var destinationMember = opt.DestinationMember as PropertyInfo;
var source = typeof(TSource);
var sourceProperty = source.GetProperty(destinationMember.Name);
if (sourceProperty != null)
{
opt.MapFrom((src, _, _, context) =>
{
string result = sourceProperty.GetValue(src) as string; // Get value from source as if it was the mapper
if (context.Options.Items.TryGetValue(LANGUAGE, out object contextLanguage) && contextLanguage is Guid languageId)
{
var translations = getTranslations(src);
if (translations != null)
{
var translation = translations.FirstOrDefault(t => t.LanguageId == languageId);
if (translation != null)
{
var value = getValue(translation);
if (!String.IsNullOrWhiteSpace(value))
{
result = value;
}
}
}
}
return result;
});
}
else
{
throw new Exception($"Can't map property {opt.DestinationMember.Name} from {source.Name}");
}
}
Let's re-define configuration without using extension method, trying to simplify things. Following mapping example, we can implement custom IValueResolver
cfg.CreateMap<TranslatableClass, TranslatedClass>()
.ForMember(dest => dest.OtherEntityLabel, opt => opt.MapFrom<CustomResolver>();
Implementing IValueResolver<TranslatableClass, TranslatedClass, string> interface:
public class CustomResolver: IValueResolver<TranslatableClass, TranslatedClass, string>
{
public string Resolve(TranslatableClass source, TranslatedClass destination, string member, ResolutionContext context)
{
string result = source.Label; /* needed effect! */
/* can we simplify this condition? */
if (context.Options.Items.TryGetValue(source.OtherEntityLabel, out object contextLanguage)
&& contextLanguage is Guid languageId)
{
var translations = source.OtherEntityTranslations;
var translation = translations.FirstOrDefault(t => t.LanguageId == languageId);
if (translation != null)
{
result = translation.Label;
};
}
return result;
}
}
Here comes same logic from
MapFromTranslation<TSource, TDestination, TMember, ... extension method provided below, let's put that logic right - we map TSource as TranslatableClass to TDestination as TranslatedClass.
Also, I believe that if (context.Options.Items.TryGetValue(...)) should be removed for simplicity too (are we trying to get languageId here?)
So, by using Custom Value Resolvers feature we can simplify mapper configuration and refactor for test coverage or debugging needs.
Update
I do want to use this extensions method on 50 others entity and I
won't write custom resolver for each one
Using Expressions instead of reflection should help to implement 'generic solution'. Solution is to define a cache of mapping expressions to access TSource and TDestination properties.
public static class MappingCache<TFirst, TSecond>
{
static MappingCache()
{
var first = Expression.Parameter(typeof(TFirst), "first");
var second = Expression.Parameter(typeof(TSecond), "second");
var secondSetExpression = MappingCache.GetSetExpression(second, first);
var blockExpression = Expression.Block(first, second, secondSetExpression, first);
Map = Expression.Lambda<Func<TFirst, TSecond, TFirst>>(blockExpression, first, second).Compile();
}
public static Func<TFirst, TSecond, TFirst> Map { get; private set; }
}
Next, let's try to define generic lambda-expressions for both
Func<TTranslation, string> getValue and getTranslations(...
e.g.:
public static Expression GetSetExpression(ParameterExpression sourceExpression, params ParameterExpression[] destinationExpressions)
{
/** AutoMapper also can be used here */
/* compile here all (source)=>(destination) expressions */
var destination = destinationExpressions
.Select(parameter => new
{
Parameter = parameter,
Property = parameter.Type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.FirstOrDefault(property => IsWritable(property) && IsOfType(property, sourceExpression.Type))
})
.FirstOrDefault(parameter => parameter.Property != null);
if (destination == null)
{
throw new InvalidOperationException(string.Format("No writable property of type {0} found in types {1}.", sourceExpression.Type.FullName, string.Join(", ", destinationExpressions.Select(parameter => parameter.Type.FullName))));
}
/* Here is the generic version of mapping code! */
return Expression.IfThen(
Expression.Not(Expression.Equal(destination.Parameter, Expression.Constant(null))),
Expression.Call(destination.Parameter, destination.Property.GetSetMethod(), sourceExpression));
}
Next goes IsWritable(PropertyInfo property) that is used to check validate properties, try to implement convention-based property filtering (names, attributes, etc.) here
public static bool IsWritable(PropertyInfo property)
{
/* eliminating reflection code from extension method */
return property.CanWrite && !property.GetIndexParameters().Any();
}
Next IsOfType(PropertyInfo... and IsSubclassOf methods, - define simple rules of proper TSource->TDestination ways of mapping...
public static bool IsOfType(PropertyInfo property, Type type)
{
/* here AutoMapper could be used too, making filtering needed destination entities by following some convention */
return property.PropertyType == type || IsSubclassOf(type, property.PropertyType) || property.PropertyType.IsAssignableFrom(type);
}
public static bool IsSubclassOf(Type type, Type otherType)
{
return type.IsSubclassOf(otherType);
}
}
Trying to implement convention based mapping approach:
public static void MapFromTranslation<TSource, TDestination, TMember, TTranslation>(this IMemberConfigurationExpression<TSource, TDestination, TMember> opt, Expression<Func<TSource, TDestination, TMember, TTranslation>> mapping )
where TTranslation : ITranslation
Wiring around the Expression<Func<TSource,TDestination,TMember, TTranslation> mapping and the MappingCache<TSource,TDestination,TMember, TTranslation>.Map is the next step. Our lambda expression represents the property transformation intent generically (mapping,conversion,validating,navigating, etc...), and when compiled lambda called with parameters passed, we get the result of such transformation.
Expression:
MappingCache<TSource,TDestination,TMember, TTranslation>.GetSetExpression(first, second, third, proxy...
Function:
var result = MappingCache<TSource,TDestination,TMember, TTranslation>.Map(first,second,third,...
Keeping statically compiled lambda-delegates abstractions open, we can cover every needed mapping aspect with proper tests, - seems like the generic approach that could be used to address the question
Access default map function in MapFrom to fallback
(c) Dapper.Mapper + Tests

How to set field of <T> from a List<string> with IEnumerable.Repeat

I have a DTO that contains several List<> of other DTO that I want to initialize to a known state. Each of the DTO contains a Description member which I also want to initialize with default values.
Example DTO (Typical):
public class ResidentialDescriptionItemDto : EntityDto<int>
{
...
public string Description { get; set; }
...
}
Default Settings
private List<ResidentialDescriptionItemDto> _residentialDescriptionItems;
private List<string> _residentialBuildingsList = new List<string>
{
"Living Room",
"Kitchen",
"Dining Room",
"Master Bed",
"Bedroom 2",
"Bedroom 3",
"Bedroom 4",
"Bathroom 1",
"Bathroom 2",
"Other"
};
Parent DTO Getter/Setter (Typical)
The private backing field for each List<T> member has been initialized to an empty List. The Getter may not be accessed if this is being instantiated with entity data. In that case the setter initializes all lists and the Getter should return that data as-is.
The following logic accomplishes the above with the exception of setting List members' .Description property in the Repeated<T>() Func.
public virtual IList<ResidentialDescriptionItemDto> ResidentialDescriptionItems
{
get => _residentialDescriptionItems = _residentialDescriptionItems.Any()
? _residentialDescriptionItems
: Repeated(new ResidentialDescriptionItemDto(), 10, _residentialBuildingsList);
set => _residentialDescriptionItems = value;
}
public static List<T> Repeated<T>(T value, int count, List<string> defaultLabels = null)
{
count = defaultLabels?.Count ?? count;
// Create list with count items.
var ret = new List<T>(count);
ret.AddRange(Enumerable.Repeat(value, count));
==>>// How can I set the .Description field of each T according to the defaultLabels parm?
return ret;
}
How can I expand on Repeated() function with short/simple Lambda/Linq to accomplish the above?
Create interface:
public interface IDescription
{
string Description { get; set; }
}
Implement interface in your DTO:
public class ResidentialDescriptionItemDto : EntityDto<int>, IDescription
{
public string Description { get; set; }
}
Change method to following:
public static List<T> Repeated<T>(List<string> defaultLabels = null)
where T : IDescription, new()
{
if (defaultLabels == null)
return new List<T>();
return defaultLabels
.Select(x => new T { Description = x })
.ToList();
}
It works.
Another approach. Without interface.
public static List<T> Repeated<T>(
Func<string, T> func, List<string> defaultLabels = null)
where T : new()
{
if (defaultLabels == null)
return new List<T>();
return defaultLabels
.Select(x => func(x))
.ToList();
}
func is Factory method.
Use it like this:
var result = Repeated<ResidentialDescriptionItemDto>(
x => new ResidentialDescriptionItemDto { Description = x }, list);

Automapper convert list of system object

I need to convert one class to another using automapper and my objects looks like this:
public class Foo
{
public List<object> Objects { get; set; }
}
public class Bar
{
public List<object> Objects { get; set; }
}
public class FooItem
{
public string Name { get; set; }
}
public class BarItem
{
public string Name { get; set; }
}
There could be more types for items, not just FooItem and BarItem but I just use these two for simplicity and I need to have this design.
I've tried several things like type converters and so on but still no luck.
Here the current basic conversion code
Mapper.Initialize(cfg =>
{
cfg.CreateMap<FooItem, BarItem>();
cfg.CreateMap<Foo, Bar>();
});
var foo = new Foo()
{
Objects = new List<object>() { new FooItem() { Name = "name" } }
};
var map = Mapper.Map<Foo, Bar>(foo);
The goal is that the Bar object contains a list of BarItems at runtime, but so far I only have only managed to get a list of FooItem at runtime.
Any ideas?
Are you satisfied with only a list of BarItems? In this case you can do it like this:
var map = Mapper.Map<IEnumerable<FooItem>, List<BarItem>>(foo.Objects.Cast<FooItem>());
Update: You can do it like this.
class Program
{
static void Main(string[] args)
{
Mapper.Initialize(cfg =>
{
cfg.CreateMap<FooItem, BarItem>();
cfg.CreateMap<Foo, Bar>()
.ForMember(dest => dest.Objects, opt => opt.ResolveUsing<CustomResolver>());
});
Mapper.AssertConfigurationIsValid();
var foo = new Foo()
{
Objects = new List<object>() { new FooItem() { Name = "name" } }
};
//var map = Mapper.Map<IEnumerable<FooItem>, List<BarItem>>(foo.Objects.Cast<FooItem>());
var map = Mapper.Map<Foo, Bar>(foo);
}
}
public class CustomResolver : IValueResolver<Foo, Bar, List<object>>
{
public List<object> Resolve(Foo source, Bar destination, List<object> member, ResolutionContext context)
{
var map = Mapper.Map<IEnumerable<FooItem>, List<BarItem>>(source.Objects.Cast<FooItem>());
return map.Cast<object>().ToList();
}
}
You can do type checks inside a ConstructUsing clause:
cfg.CreateMap<object, object>()
.ConstructUsing(src => {
if (src is FooItem) {
return Mapper.Map<BarItem>(src);
}
// ...
throw new InvalidOperationException($"Can not map source item of type '{src.GetType().FullName}'.");
});
But you probably need to introduce an interface for the items in the Objects collection, because the map object -> object overrides all other maps, but we want to use the map FooItem -> BarItem.
Inspired by #Ogglas I manage to found out an interesting solution where it is not needed to check for all the types.
Basically I wrote my own ITypeConverter
public class CustomResolver : ITypeConverter<List<object>, List<object>>
{
public List<object> Convert(List<object> source, List<object> destination, ResolutionContext context)
{
var objects = new List<object>();
foreach (var obj in source)
{
var destinationType = context.ConfigurationProvider.GetAllTypeMaps().First(x => x.SourceType == obj.GetType()).DestinationType;
var target = context.Mapper.Map(obj, obj.GetType(), destinationType);
objects.Add(target);
}
return objects;
}
}
And get from the context the proper mapping for each type. Currently is not aware of nulls and so on...
Then, when configuring automapper
Mapper.Initialize(cfg =>
{
cfg.CreateMap<FooItem, BarItem>();
cfg.CreateMap<List<object>, List<object>>().ConvertUsing<CustomResolver>();
cfg.CreateMap<Foo, Bar>();
});

Get list of distinct values in List<T> in c#

So, say I have something like the following:
public class Element
{
public int ID;
public int Type;
public Properties prorerty;
...
}
and
public class Properties
{
public int Id;
public string Property;
...
}
and I have a list of these:
List Elements = new List();
What would be the cleanest way to get a list of all distinct values in the prorerty column in Element class? I mean, I could iterate through the list and add all values that aren't duplicates to another list of strings, but this seems dirty and inefficient. I have a feeling there's some magical Linq construction that'll do this in one line, but I haven't been able to come up with anything.
var results = Elements.Distinct();
Note: you will have to override .Equals and .GetHashCode()
public class Element : IEqualityComparer<Element>
{
public bool Equals(Element x, Element y)
{
if (x.ID == y.ID)
{
return true;
}
else
{
return false;
}
}
}
public int GetHashCode(Element obj)
{
return obj.ID.GetHashCode();
}
Isn't simpler to use one of the approaches shown below :) ? You can just group your domain objects by some key and select FirstOrDefault like below.
This is a copy of my answer on similar question here:
Get unique values - original answer
More interesting option is to create some Comparer adapter that takes you domain object and creates other object the Comparer can use/work with out of the box. Base on the comparer you can create your custom linq extensions like in sample below. Hope it helps :)
[TestMethod]
public void CustomDistinctTest()
{
// Generate some sample of domain objects
var listOfDomainObjects = Enumerable
.Range(10, 10)
.SelectMany(x =>
Enumerable
.Range(15, 10)
.Select(y => new SomeClass { SomeText = x.ToString(), SomeInt = x + y }))
.ToList();
var uniqueStringsByUsingGroupBy = listOfDomainObjects
.GroupBy(x => x.SomeText)
.Select(x => x.FirstOrDefault())
.ToList();
var uniqueStringsByCustomExtension = listOfDomainObjects.DistinctBy(x => x.SomeText).ToList();
var uniqueIntsByCustomExtension = listOfDomainObjects.DistinctBy(x => x.SomeInt).ToList();
var uniqueStrings = listOfDomainObjects
.Distinct(new EqualityComparerAdapter<SomeClass, string>(x => x.SomeText))
.OrderBy(x=>x.SomeText)
.ToList();
var uniqueInts = listOfDomainObjects
.Distinct(new EqualityComparerAdapter<SomeClass, int>(x => x.SomeInt))
.OrderBy(x => x.SomeInt)
.ToList();
}
Custom comparer adapter:
public class EqualityComparerAdapter<T, V> : EqualityComparer<T>
where V : IEquatable<V>
{
private Func<T, V> _valueAdapter;
public EqualityComparerAdapter(Func<T, V> valueAdapter)
{
_valueAdapter = valueAdapter;
}
public override bool Equals(T x, T y)
{
return _valueAdapter(x).Equals(_valueAdapter(y));
}
public override int GetHashCode(T obj)
{
return _valueAdapter(obj).GetHashCode();
}
}
Custom linq extension (definition of DistinctBy extension method):
// Embed this class in some specific custom namespace
public static class DistByExt
{
public static IEnumerable<T> DistinctBy<T,V>(this IEnumerable<T> enumerator,Func<T,V> valueAdapter)
where V : IEquatable<V>
{
return enumerator.Distinct(new EqualityComparerAdapter<T, V>(valueAdapter));
}
}
Definition of domain class used in test case:
public class SomeClass
{
public string SomeText { get; set; }
public int SomeInt { get; set; }
}
var props = Elements.Select(x => x.Properties).Distinct();
And make sure you overridden .Equals() and .GetHashCode() methods.
Or if you need direct strings from Properties:
var props = Elements
.Select(x => x.Properties != null ? x.Properties.Property : null)
.Distinct();
If you need the string fields on the Properties field, and if you know the Properties field prorerty is never null, just use
IEnumerable<string> uniqueStrings = Elements
.Select(e => e.prorerty.Property).Distinct();
If there's a chance prorerty can be null, handle that situation in the lambda.
This will use the default equality comparer for String which is an ordinal comparison independent of culture and case-sensitive.
my working example from LINQPad (C# Program)
void Main()
{
var ret = new List<Element>();
ret.Add(new Element(){ID=1});
ret.Add(new Element(){ID=1});
ret.Add(new Element(){ID=2});
ret = ret.GroupBy(x=>x.ID).Select(x=>x.First()).ToList();
Console.WriteLine(ret.Count()); // shows 2
}
public class Element
{
public int ID;
public int Type;
public Properties prorerty;
}
public class Properties
{
public int Id;
public string Property;
}

Categories

Resources