I've previously post my question here but I didn't get any reply cause - I guess - it's too generic. I'll try to be more concise.
I've got two object of the same type and I want to map some of the properties and exclude others. What I am trying to do is save an object in a cache and fetch it later, using only the properties (fields) with a specific attribute.
I've had a look at Automapper but I haven't found anything which seems to be suitable for me so I've thought to implement my own system.
I've created an attribute:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class FilterFieldAttribute: Attribute
{
}
and decorated a class with it on the fields I need to include:
public class OrdersViewModel : BaseViewModel
{
...
[FilterField]
[DisplayName("Order Number:")]
public string OrderNumber { get; set; }
[FilterField]
[DisplayName("From date:")]
public DateTime FromDate { get; set; }
[FilterField]
[DisplayName("To date:")]
public DateTime ToDate { get; set; }
[DisplayName("Status:")]
public int Status { get; set; }
...
}
Now, I've implemented a function which is responsible for the mapping:
private T Map<T>(T Source, T Destination) where T : BaseViewModel
{
if (Source == null)
{
return (Source);
}
FilterFieldAttribute filterAttribute;
foreach (PropertyInfo propInfo in typeof(T).GetProperties())
{
foreach (FilterFieldAttribute attr in propInfo.GetCustomAttributes(typeof(FilterFieldAttribute), false))
{
filterAttribute = attr as FilterFieldAttribute;
if (filterAttribute != null)
{
var value = propInfo.GetValue(Source, null);
propInfo.SetValue(Destination, value, null);
}
}
}
return (Destination);
}
Now, when I need to fetch my viewmodel from the cache and fill only the properties marked with the attribute my code looks like this:
viewModel = Map<T>(myCache.Get(Key) as T, viewModel);
I don't know if this is the best to do it but it seems the only way I have found.
Any suggestion would be appreciated.
Using direct reflection (as in the example) will be sloooow; giving a more complete answer is tricky, as it depends on whether you want a shallow clone or deep clone of sub-objects. Either way, you should expect this to involve some meta-programming and caching - using either ILGenerator or Expression.
However, for a lazy option, serialization might be useful. A number of serializers allow you to use attributes to include/exclude particular items; by serializing to a memory-stream, rewinding (.Position=0
) and deserializing you should get a deep copy of just your chosen members.
Here's an example using Expression
:
using System;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class FilterFieldAttribute: Attribute
{
public static T Clone<T>(T obj) where T : class, new()
{
return Cache<T>.clone(obj);
}
private static class Cache<T> where T : class, new()
{
public static readonly Func<T,T> clone;
static Cache()
{
var param = Expression.Parameter(typeof(T), "source");
var members = from prop in typeof(T).GetProperties()
where Attribute.IsDefined(prop, typeof(FilterFieldAttribute))
select Expression.Bind(prop, Expression.Property(param, prop));
var newObj = Expression.MemberInit(Expression.New(typeof(T)), members);
clone = Expression.Lambda<Func<T,T>>(newObj, param).Compile();
}
}
}
public class OrdersViewModel
{
[FilterField]
[DisplayName("Order Number:")]
public string OrderNumber { get; set; }
[FilterField]
[DisplayName("From date:")]
public DateTime FromDate { get; set; }
[FilterField]
[DisplayName("To date:")]
public DateTime ToDate { get; set; }
[DisplayName("Status:")]
public int Status { get; set; }
static void Main()
{
var foo = new OrdersViewModel { OrderNumber = "abc", FromDate = DateTime.Now,
ToDate = DateTime.Now, Status = 1};
var bar = FilterFieldAttribute.Clone(foo);
}
}
Sounds like a good start but you are loosing so much of the benefits of AutoMapper. AutoMapper can also take care of the nested properties (when your class contains nested mapped classes) which you do not have here.
Since AutoMapper is an oprn source project, I suggest you take AutoMapper source and implement filtering there. This could be in fact sueful for others too.
I think your approach is ok. Reflection has some performance implications - which is worth considering.
An alternative, performant, and simpler approach might be to have the BaseViewModel define an abstract method:
public abstract BaseViewModel ToCacheVersion();
Which can be used to convert the subclass to the correct type. Each subclass would take care of its own mapping:
public class ViewModelX
{
public ViewModelX(string name, string description)
{
Name = name;
Description = description;
}
...
public override BaseViewModel ToCacheVersion()
{
return new ViewModelX(
Name, // Include the name.
null // Ignore the description.
);
}
...
}