如何使用属性映射属性

How to use attribute to map properties

我有以下代码。

public class SyncProperty : Attribute
{
    public readonly string PropertyName;

    public SyncProperty(string propertyName)
    {
        this.PropertyName = propertyName;
    }
}

public class SyncContact
{
    [SyncProperty("first_name")]
    public string FirstName { get; set; }

    [SyncProperty("last_name")]
    public string LastName { get; set; }

    [SyncProperty("phone")]
    public string Phone { get; set; }

    [SyncProperty("email")]
    public string Email { get; set; }
}

我需要创建一个 SyncContact 实例,例如

var contact = new SyncContact { FirstName="Test", LastName="Person", Phone="123-123-1234", Email="test@test.com"};

然后我需要使用该对象创建一个 NameValueCollection,其中对象的 属性 使用 SyncProperty 的 PropertyName 作为集合中的键。然后使用它向 API 发出 post 请求。所以在这种情况下,我最终会得到一个像...

这样的集合
collection["first_name"] = "Test"
collection["last_name"] = "Person"
collection["phone"] = "123-123-1234"
collection["email"] = "test@test.com"

我该怎么做?

假设每个 属性 上都标记了 SyncProperty,这应该可以完成工作:

var contact = new SyncContact { FirstName="Test", LastName="Person", Phone="123-123-1234", Email="test@test.com"};
var collection = contact.GetType().GetProperties()
    .Select(x => new
    {
        x.GetCustomAttribute<SyncProperty>().PropertyName,
        Value = x.GetValue(contact).ToString()
    })
    .ToDictionary(x => x.PropertyName, x => x.Value);

作为辅助方法:

public static class SynxHelper
{
    public static Dictionary<string, string> Serialize<T>(T obj)
    {
        return typeof(T).GetProperties()
            .Select(x => new
            {
                SyncProperty = x.GetCustomAttribute<SyncProperty>(),
                Value = x.GetValue(obj)
            })
            .Where(x => x.SyncProperty != null)
            .ToDictionary(x => x.SyncProperty.PropertyName, x => x.Value.ToString());
    }
}

// usage
var collection = SynxHelper.Serialize(contact);

属性属于class的属性,所以需要获取class的类型,然后找到合适的属性,然后获取自定义属性。

类似于:

var contact = new SyncContact { FirstName="Test", LastName="Person", Phone="123-123-1234", Email="test@test.com"};
var t = typeof(SyncContact);
var props = t.GetProperties().Select(p => new { p, attr = p.GetCustomAttribute<SyncProperty>() }).Where(p => p.attr != null);

var dict = props.ToDictionary(p => p.attr.PropertyName, v => v.p.GetValue(contact) );

这会排除所有未标记的属性,但您可以根据需要决定以不同方式处理这些属性。

Fiddle

这是针对此问题的单行 linq 解决方案

(from prop in obj.GetType().GetProperties()
                    where prop.GetCustomAttribute<SyncProperty>() != null
                    select new { Key = prop.GetCustomAttribute<SyncProperty>().PropertyName, Value = prop.GetValue(obj) })
                    .ToDictionary(k => k.Key, v => v.Value);

但是!!!!!!!!!不要尝试自己这样做。这没有像所有反射一样优化和缓慢。 这只是为了证明反射有多糟糕

static void Main(string[] args)
        {
            var data = Enumerable.Range(0, 10000).Select(i => new SyncContact { FirstName = "Test", LastName = "Person", Phone = "123-123-1234", Email = "test@test.com" }).ToArray();

            Stopwatch sw = new Stopwatch();
            long m1Time = 0;
            var total1 = 0;
            sw.Start();
            foreach (var item in data)
            {
                var a = ToSyncDictionary(item);
                total1++;
            }
            sw.Stop();
            m1Time = sw.ElapsedMilliseconds;
            sw.Reset();
            long m2Time = 0;
            var total2 = 0;
            sw.Start();
            foreach (var item in data)
            {
                var a = ToSyncDictionary2(item);
                total2++;
            }
            sw.Stop();
            m2Time = sw.ElapsedMilliseconds;

            Console.WriteLine($"ToSyncDictionary : {m1Time} for {total1}");
            Console.WriteLine($"ToSyncDictionary2 : {m2Time} for {total2}");
            Console.ReadLine();
        }

        public static IDictionary<string, string> ToSyncDictionary<T>(T value)
        {
            var syncProperties = from p in typeof(T).GetProperties()
                                 let name = p.GetCustomAttribute<SyncProperty>()?.PropertyName
                                 where name != null
                                 select new
                                 {
                                     Name = name,
                                     Value = p.GetValue(value)?.ToString()
                                 };

            return syncProperties.ToDictionary(p => p.Name, p => p.Value);
        }

        public static IDictionary<string, string> ToSyncDictionary2<T>(T value)
        {
            return Mapper<T>.ToSyncDictionary(value);
        }

        public static class Mapper<T>
        {
            private static readonly Func<T, IDictionary<string, string>> map;

            static Mapper()
            {
                map = ObjectSerializer();
            }

            public static IDictionary<string, string> ToSyncDictionary(T value)
            {
                return map(value);
            }

            private static Func<T, IDictionary<string, string>> ObjectSerializer()
            {
                var type = typeof(Dictionary<string, string>);
                var param = Expression.Parameter(typeof(T));
                var newExp = Expression.New(type);

                var addMethod = type.GetMethod(nameof(Dictionary<string, string>.Add), new Type[] { typeof(string), typeof(string) });
                var toString = typeof(T).GetMethod(nameof(object.ToString));

                var setData = from p in typeof(T).GetProperties()
                              let name = p.GetCustomAttribute<SyncProperty>()?.PropertyName
                              where name != null
                              select Expression.ElementInit(addMethod,
                                                            Expression.Constant(name),
                                                            Expression.Condition(Expression.Equal(Expression.Property(param, p), Expression.Constant(null)),
                                                                       Expression.Call(Expression.Property(param, p), toString),
                                                                       Expression.Constant(null,typeof(string))));

                return Expression.Lambda<Func<T, IDictionary<string, string>>>(Expression.ListInit(newExp, setData), param).Compile();
            }
        }

在我的机器上,我得到了 10 倍的提升。 如果您可以使用像 JSON.net 这样的序列化程序,因为您需要更改很多东西才能使其正常工作,并且您已经有了适合您的东西。

如果你想获取一些类型元数据,你应该使用 Reflection. To read attribute you can use GetCustomAttribute<AttributeType>() MemberInfo 扩展。

此扩展方法从用 SyncPropertyAttribute 修饰的类型属性构建同步字典(我建议使用字典而不是 NameValueCollection):

public static Dictionary<string, string> ToSyncDictionary<T>(this T value)
{
    var syncProperties = from p in typeof(T).GetProperties()
                         let name = p.GetCustomAttribute<SyncProperty>()?.PropertyName
                         where name != null
                         select new {
                             Name = name,
                             Value = p.GetValue(value)?.ToString()
                         };

    return syncProperties.ToDictionary(p => p.Name, p => p.Value);
}

用法:

var collection = contact.ToSyncDictionary();

输出:

{
  "first_name": "Test",
  "last_name": "Person",
  "phone": "123-123-1234",
  "email": "test@test.com"
}

注意:如果您打算在 POST 请求中使用联系人数据,那么您应该考虑使用简单的 JSON 序列化属性,而不是创建您自己的属性。例如。 Json.NET:

public class SyncContact
{
    [JsonProperty("first_name")]
    public string FirstName { get; set; }

    [JsonProperty("last_name")]
    public string LastName { get; set; }

    [JsonProperty("phone")]
    public string Phone { get; set; }

    [JsonProperty("email")]
    public string Email { get; set; }
}

然后简单的序列化就可以完成工作:

string json = JsonConvert.SerializeObject(contact);

结果将与上面完全相同。