【问题标题】:Pass Lambda to Chain Over Object Members to Function将 Lambda 传递给将对象成员链接到函数
【发布时间】:2014-03-27 22:42:24
【问题描述】:

动机

我有一个插件,它通过复制旧实体字段并以各种方式更新新实体字段来更新实体。我的问题是并非所有字段都以相同的方式更新。例如,并非所有日期的年份都增加 1。一半,最后两个季度被复制或清除。我正在为 CRM 2011 使用早期绑定的实体类。我想利用这些类以一种编译时安全的方式将实体的字段分组到列表中。给定任何 entity.FieldName,我可以提取 AttributeCollection 的字符串键。现在我需要一些构造、语法糖化、C#中的东西——我还没有找到能够将这些字段的访问链接到字符串键名提取器中。 (希望我的示例希望代码可以消除任何混淆)。

实体表单的同一部分中相同类型的字段通常具有相同类型的更新,但该逻辑顺序在早期绑定类中不可用。例如,某个可折叠菜单中的所有日期字段都被清除,但实体类不知道表单级别的分组,因为它都是 XML。

我可以遍历新旧实体的 Attributes 集合,比较每个匹配键的值,并显示差异,但我认为这需要太多手动验证。它不能进行良好的自动化测试。

我真正想做的事

我希望能够只指定已更新的字段,而不必为实体的 AttributeCollection 字典键入字符串键。我希望我可以从 VB 中借用或找到相应的语法(using 语法)或类似于通过点表示法对实体字段的链式调用(参见代码示例)。

这是我想做的一个示例

感谢 this question 提供的 GetPropertyName 方法。太好了!也许需要稍微改变 lambda 以获取多个参数...

// Gets the string name of whatever object or class property is passed in a lambda
// Usage: GetPropertyName( () => someObject.Property ); ==> "Property"
public string GetPropertyName<T>(Expression<Func<T>> propertyLambda)

// normal use of GetPropertyName is quite helpful
// assume early_bound_entity has fields: name, start_date, end_date, and currency
var e = new early_bound_entity();
string s = GetPropertyName( () => e.start_date); // s => "start_date"

// The "block" I do inside of GetPropertyName is what I am hoping to be able to do!
// i.e. pass as many System.Reflection.MemberInfo instances to GetPropertyName as I can
// I really want to NOT have to say e.field, e.anotherfield, ... but C# does not have VB's using syntax :(
List<string> fieldsShouldHaveCopied = GetPropertyName( (early_bound_entity e) => e.name, e.start_date, e.end_date, e.currency );
// fieldsShouldHaveCopied => { "name", "start_date", "end_date", "currency" }

为什么会这样?

我希望我可以使用我的早期绑定实体的类定义并围绕我的早期绑定对象设置一个上下文。然后,使用其智能感知和类定义,限制将哪些字段组合在一起。我真的不想输入后期绑定的 Entity.Attributes 字符串键,因为这样维护起来会很糟糕——没有智能感知、没有编译时验证、字符串可能拼写错误等等。我可以检查 AttributeCollection 是否包含key first 并警告使用了错误的密钥(无论如何都会这样做),但是未来的开发人员将被困在维护“手写”字符串文字列表中。 当我通过 early_bound_entity 字段传递某种表达式链接时,我希望 GetPropertyName 方法生成这些字符串列表,我希望从代码示例中可以清楚地看到这一点。

如果我滥用术语块、闭包和 lambda,我很抱歉,只要我提到它们中的任何一个,就将它们组合在一起。

另外,直接告诉我这是不是一个[愚蠢|可怕|等等]的想法,并告诉我为什么!我面临着必须通过点符号(甚至是它们的字符串键)输入约 300 个字段访问器来验证它们是否已更新的前景。很有可能这个实体太大了,但遗憾的是现状。

谢谢!

【问题讨论】:

  • 您这样做只是为了引用后期绑定实体中的属性名称吗?
  • 是的,我是。我知道我可以将后期绑定的字段作为字符串输入,并在尝试使用它们之前检查它们是否在 Attributes 字典中。但我认为这很难维持。至少这种方式有某种编译时安全的方式来访问它们。也许是环形交叉路口。

标签: c# unit-testing dynamics-crm-2011


【解决方案1】:

您可以创建自己的表达式访问者,该访问者将能够跟踪从参数访问成员的所有成员访问。

internal class ParameterAccessesVisitor : ExpressionVisitor
{
    private ReadOnlyCollection<ParameterExpression> parameters;
    public ParameterAccessesVisitor(
        ReadOnlyCollection<ParameterExpression> parameters)
    {
        VisitedMembers = new List<MemberInfo>();
        this.parameters = parameters;
    }
    protected override Expression VisitMember(MemberExpression node)
    {
        if (parameters.Contains(node.Expression))
            VisitedMembers.Add(node.Member);
        return base.VisitMember(node);
    }
    public List<MemberInfo> VisitedMembers { get; private set; }
}

然后我们可以创建一个方法来使用这个接受一个表达式(应该是一个 lambda 表达式)的访问者,拉出参数,找到对这些参数的所有成员访问,然后返回序列:

public static IEnumerable<MemberInfo> ParameterAccesses<T, TResult>(
    Expression<Func<T, TResult>> lambda)
{
    var visitor = new ParameterAccessesVisitor(lambda.Parameters);
    visitor.Visit(lambda);
    return visitor.VisitedMembers;
}

如果您只想要成员名称,而不是完整的MemberInfo,那么一个简单的Select 可以将名称选择到一个数组中。

我们现在可以写:

var members = ParameterAccesses((List<int> p) => new { p.Count, p.Capacity })
    .Select(member => member.Name);

然后打印出来:

计数

容量

由于您想从这些类型中获取给定属性的值,您可以轻松地添加另一个方法来基于上述方法执行此类查询:

public static IEnumerable<string> GetLogicalNames<T, TResult>(
    Expression<Func<T, TResult>> lambda)
{
    return from member in ParameterAccesses(lambda)
            let attributes = member.GetCustomAttributes<AttributeLogicalNameAttribute>()
            where attributes.Any()
            select attributes.First().LogicalName;
}

【讨论】:

  • 你把所有的结果都变成了一个结果!我很高兴能尝试一下。
  • 这个答案总体上是 awesome,但@Nicknow 的答案在这种特殊情况下是最有用的,因为它是特定于 CRM 的。
  • @gfritz 鉴于您拥有MemberInfo 对象,而不仅仅是名称,您需要做的就是添加Select 以从每个成员中获取属性。没有必要仅仅为了那个改变就重构整个类,如果这是你需要的,那就增加 30 个字符。
  • 真@Servy。我的错。乍一看,您和@Nicknow 的答案非常相似,主要区别在于他的GetLogicalNames 方法完全符合我的要求。考虑到您首先回答,他扩展(并特别引用)了您的答案,并且为了得到我想要的内容,除了您所说的大约 30 个字符,我会将您的作为答案。
【解决方案2】:

当使用CrmSvcUtil.exe 时,无法保证早期绑定的属性名称与 CRM 中的逻辑名称完全匹配。一般来说,它应该,但这不能保证——当然不能保证它们是大小写匹配的。我相信 C# 属性使用架构名称,Entity 属性逻辑名称使用 CRM 的逻辑名称。可能存在大小写差异。此外,您可以编写额外的代码来修改CrmSvcUtil.exe使用的命名约定

因此,属性名称可能与属性名称不匹配。例如:

var su = new SystemUser();
su.FirstName = "Mike Jones";
Console.WriteLine(su.Attributes["FirstName"]);

将导致 System.Collections.Generic.KeyNotFoundException 异常。

FirstName 属性作为属性引用的正确方法是:

Console.WriteLine(su.Attributes["firstname"]);

好消息是 Dynamics CRM 早期绑定对象的每个属性都有一个名为 Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute 的代码属性。这定义了 CRM 中属性的逻辑名称,如下所示:

[Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("activityid")]

您可以对@Servy 的逻辑进行一些更改以获取实际的 CRM 逻辑名称。

将此类添加到您的应用程序中:

public class CrmEntityLogicalName : ExpressionVisitor
    {    

        private ReadOnlyCollection<ParameterExpression> parameters;
        private CrmEntityLogicalName(
            ReadOnlyCollection<ParameterExpression> parameters)
        {
            VisitedMembers = new List<MemberInfo>();
            this.parameters = parameters;
        }

        protected override Expression VisitMember(MemberExpression node)
        {
            if (parameters.Contains(node.Expression))
            {
                if (Attribute.IsDefined(node.Member, typeof(AttributeLogicalNameAttribute)))
                {
                    VisitedMembers.Add(node.Member);
                }                
            }
            return base.VisitMember(node);
        }
        private List<MemberInfo> VisitedMembers { get; set; }

        public static IEnumerable<string> GetLogicalNames<T, TResult>(
            Expression<Func<T, TResult>> lambda)
        {
            var visitor = new CrmEntityLogicalName(lambda.Parameters);
            visitor.Visit(lambda);

            return (from entMember in visitor.VisitedMembers select entMember.GetCustomAttributes(typeof (AttributeLogicalNameAttribute), false) 
                        as AttributeLogicalNameAttribute[] into atts where atts != null && atts.Any() select atts[0].LogicalName).ToList();
        }
    }

这是如何在您的应用程序中调用代码的示例:

var entMembers = CrmEntityLogicalName.GetLogicalNames((SystemUser sysuser) =&gt; new { sysuser.CreatedBy, sysuser.CreatedOn, sysuser.FirstName });

这将返回以下列表:

这将保证您始终拥有正确的属性逻辑名称,而不是 C# 中的属性名称。

【讨论】:

  • 对!我忘记了可能与实体逻辑名称不匹配的 c# 名称。这是一个很好的扩展。
  • 这就像一个魅力。非常感谢您的帮助!
  • 对答案标记交换感到抱歉。你和@Servy 的回答都真的很好,但我觉得他的回答总体上是最有用的。你让它专门为 CRM 工作,而他得到了它,所以我可以做我想要的 lambda 事情,这是我在这里的主要目标。
  • 不用担心,我同意 @Servy 值得称赞,因为他的回答是我回答的前提。如果他没有发布他的答案,我可能不会想出我的答案。
【解决方案3】:

您还可以创建自己的 ICustomizeCodeDomService,将属性名称创建为早期绑定的字符串属性,然后从 CrmSrvcUtil 中引用它。

当然它看起来不太好:

List<string> fieldsShouldHaveCopied = new List<string>(
{
    AttributeNames.Contract.Name, 
    AttributeNames.Contract.Start_date,
    AttributeNames.Contract.End_date,
    AttributeNames.Contract.Currency
});

【讨论】:

    猜你喜欢
    • 2013-12-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-09-27
    相关资源
    最近更新 更多