1

Motivation

I have a plugin which renews an entity by copying the old on and updating the new entity fields in various ways. My problem is that not all fields are updated the same way. For example, not all dates have their year incremented by 1. Half do, and the last two quarters are copied or cleared. I am using the early-bound entity classes for CRM 2011. I want to leverage those classes to have a compile-time safe way to group the fields of the entities into lists. I can extract the string key for the AttributeCollection given any entity.FieldName. Now I need some construct, syntactic sugaring, thing-in-C#-that-I-haven't-found to be able to chain accessing those fields into the string key name extractor. (hopefully my example Want code clears up any confusion).

Fields of the same type within the same section of the entity form generally have the same kind of update, but that logical ordering is not available in the early-bound class. For example, all of the date fields within a certain collapsible menu all get cleared, but the entity class does not have any knowledge of the form level grouping because it is all XML.

I could loop over the Attributes collection of the old and new entity, comparing the value of each matching key, and display the differences, but I think that requires too much manual validation. It does not make a good automated test.

What I Actually Want To Do

I would like to be able to specify only the fields that were updated without having to type out the string keys for the entity's AttributeCollection dictionary. I was hoping I could borrow or locate a corresponding a syntax from VB (the using syntax) or something similar to chain calls to the entity's fields via dot notation (see the code example).

HERE IS A SAMPLE OF WHAT I WANT TO DO

Thanks to this question for the GetPropertyName method. It is great! Maybe the lambda will need to be altered slightly to take multiple parameters...

// 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" }

WHY this way?

I was hoping I could use the class definition of my early-bound entity and set up a context around my early-bound object. Then, using its intellisense and class definition, restrict what fields are grouped together. I really do not want to type out the late-bound Entity.Attributes string keys because that would be horrible to maintain - no intellisense, no compile-time verification, the string could be misspelled, etc. I can check that the AttributeCollection contains the key first and warn that a bad key was used (and will do that anyway), but then future developers would be stuck maintaining "hand-written" Lists of string literals. I would prefer the GetPropertyName method generate those string lists, when I pass in some kind of expression chaining through the early_bound_entity fields which I hope was clear from the code example.

I apologize if I have abused the terms block, closure, and lambda by grouping them together whenever I mention any one of them.

Also, go right ahead and tell me if this is a [stupid|terrible|etc.] idea, and tell me why! I am faced with the prospect of having to type out ~300 field accessors via dot notation (or even their string keys ) just to verify that they were updated. It is quite possible that this entity is too big, but that is sadly the status quo.

Thanks!

Community
  • 1
  • 1
gfritz
  • 559
  • 9
  • 16
  • Are you just doing this to reference attribute names in a late bound entity? – Daryl Mar 27 '14 at 11:20
  • Yes I am. I know I could just type out the late bound fields as strings and check if they are in the Attributes dictionary before trying to use them. But I think that would be tough to maintain. At least this way there is some sort of compile-time safe way to access them. Perhaps it is roundabout. – gfritz Mar 27 '14 at 12:09

3 Answers3

2

You can create your own expression visitor that will be able to keep track of all member accesses where the member is being accessed from a parameter.

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; }
}

We can then create a method to use this visitor that accepts an expression (which should be a lambda expression), pulls out the parameters, find all member accesses to those parameters, and then returns the sequence:

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

If you want just the member names, rather than the full MemberInfo, then a simple Select can select out the names into an array.

We can now write:

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

And have it print out:

Count

Capacity

Since you want to grab a value of a given attribute from these types, you can easily add another method to perform such a query based on the above method:

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;
}
Community
  • 1
  • 1
Servy
  • 202,030
  • 26
  • 332
  • 449
  • You rolled it all into one result! I am excited to try this out. – gfritz Mar 27 '14 at 00:41
  • This answer is **awesome** in general, but @Nicknow's answer was the most useful in this particular case, since it was CRM specific. – gfritz Mar 27 '14 at 13:54
  • @gfritz Given that you had the `MemberInfo` objects, and not just the name, all you need to do is add a `Select` to get the attribute from each of those members. There's no need to refactor the whole class just for that one change, and if that's something you needed, it's a 30 character addition. – Servy Mar 27 '14 at 13:57
  • True @Servy. My mistake. At first glance yours and @Nicknow's answers were really similar with the primary difference being his `GetLogicalNames` method which did exactly what I wanted out of the box. Considering that you answered first, he extended (and specifically referenced) your answer, and the addition to get exactly what I want is ~30 characters like you said, I'll mark yours as the answer. – gfritz Mar 27 '14 at 14:32
1

There is no guarantee that an early-bound property name will exactly match the logical name in CRM when using CrmSvcUtil.exe. Generally, it should, but this is not guaranteed - and they are certainly not guaranteed to be case matched. The C# properties, I believe, use the Schema Name and the Entity attributes logical name uses CRM's logical name. There can be case differences. In addition, you can write additional code to modify the naming convention used by CrmSvcUtil.exe

Therefore, the property's name may not match the attribute name. For example:

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

Will result in a System.Collections.Generic.KeyNotFoundException exception.

The correct way to reference the FirstName property as an Attribute is:

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

The good news is that Dynamics CRM early-bound objects have a code attribute on each property called Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute. This defines the logical name of the attribute in CRM, like so:

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

You can user @Servy's logic with some changes to get actual CRM logical name.

Add this class to your application:

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();
        }
    }

This is an example of how to call the code in your application:

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

Which will return the following list:

createdby, createdon, firstname

This will guarantee that you always have the correct Attribute logical name, not the Property's name in C#.

Nicknow
  • 7,154
  • 3
  • 22
  • 38
  • 1
    Right! I forgot about the c# name possibly not matching the entity logical name. This is a good extension. – gfritz Mar 27 '14 at 01:16
  • 1
    Sorry about the answer marking swap. Both yours and @Servy's answers are **really** good, but I feel like his answer was the most useful overall. You got it to work specifically for CRM, and he got it so I could do the lambda thing that I wanted which was my primary goal here. – gfritz Mar 27 '14 at 14:37
  • No worries, I __agree__ that @Servy deserved the credit as his answer was the premise for my answer. If he hadn't posted his answer I probably would not have figured out my answer. – Nicknow Mar 27 '14 at 15:51
0

You could also create your own ICustomizeCodeDomService that creates the attribute names as early bound string properties, and then reference it from the CrmSrvcUtil.

Of course it doesn't look quite as nice:

List<string> fieldsShouldHaveCopied = new List<string>(
{
    AttributeNames.Contract.Name, 
    AttributeNames.Contract.Start_date,
    AttributeNames.Contract.End_date,
    AttributeNames.Contract.Currency
});
Daryl
  • 18,592
  • 9
  • 78
  • 145