Search code examples
c#linqdynamics-crm-2011dynamics-crm-onlinepartial-classes

Why are the properties of my partial class empty after I've run my LINQ query?


I'm pulling a list of Project entities from Dynamics CRM 2011 via the SDK like so:

var projects = (from project in Context.ProjectSet
               where project.ModifiedOn > DateTime.Now.AddHours(-1)
               select new Project
               {
                   Id = project.Id
               }).ToList();

This works fine and returns the correct number of projects and their id's.

The Project class was generated automatically by CrmSvcUtil.exe and is stored in a partial class like so:

Entities.cs
(The real file is much larger, I've cut it down)

[assembly: Microsoft.Xrm.Sdk.Client.ProxyTypesAssembly()]

namespace Framework.CRM
{
    public partial class Project : Microsoft.Xrm.Sdk.Entity, System.ComponentModel.INotifyPropertyChanging, System.ComponentModel.INotifyPropertyChanged
    {
        // There are lots of properties and events etc here
        // Here's one example of a property for brevity
        [Microsoft.Xrm.Sdk.AttributeLogicalName("createdby")]
        public Microsoft.Xrm.Sdk.EntityReference CreatedBy
        {
            get
            {
                return GetAttributeValue<Microsoft.Xrm.Sdk.EntityReference>("createdby");
            }
        }
    }
}

Several of the auto-generated properties don't have setters so I can't store their values when I run my query. As it's auto-generated code I'm not modifying it and instead extending the class with some extra properties like so:

CustomProperties.cs

namespace Framework.CRM
{
    using System;

    public partial class Project
    {
        public Guid CustomerGuid { get; set; }
        public DateTime? LastUpdated { get; set; }
        // It's only the nullable DateTime because CRM implements 
        // that for some reason even though it's never null
    }
}

I can now use the same query but this time I can store some properties that I couldn't before:

var projects = (from project in Context.ProjectSet
               where project.ModifiedOn > DateTime.Now.AddHours(-1)
               select new Project
               {
                   Id = project.Id,
                   LastUpdated = project.ModifiedOn,
                   CustomerGuid = project.CompanylinkId != null ? project.CompanylinkId.Id : Guid.Empty
               }).ToList();

This still fetches the correct number of projects and the Id property is populated but all of my custom properties are empty and I can't understand why. I can guarantee you that ModifiedOn and CompanylinkId are not empty in CRM.

For example:

foreach (var project in projects)
{
    Console.WriteLine(project.Id.ToString());
    Console.WriteLine(project.LastUpdated?.ToString("dd/MM/yyyy"));
}

Output:

{40e74702-d17b-4598-b4a0-150b353459a4}
 
{134aa5bd-aef3-4489-93c2-a3c0bdebd20a}
 

Expected:

{40e74702-d17b-4598-b4a0-150b353459a4}
18/11/2016
{134aa5bd-aef3-4489-93c2-a3c0bdebd20a}
17/11/2016

What's more frustrating is that this worked last time I did it and I have the older project sat side by side with the new one and can't see what I must have done to fix it. Am I using partial classes combined with LINQ queries incorrectly somehow?

Edit:

Interestingly if I change the query to select the existing project rather than creating a new one it works just fine:

var projects = (from project in Context.ProjectSet
               where project.ModifiedOn > DateTime.Now.AddHours(-1)
               select project).ToList();

I don't really want to do this as I only want about six fields out of hundreds and this is the equivalent of select * from.


Solution

  • This is one of my frustrations with the standard CrmSvcUtil generated classes. They don't allow for setting of readonly fields, unless you retrieve the entire class, which is a bad practice.

    To help overcome this issue, I wrote a change in the EarlyBoundGenerator for the XrmToolBox to "Generate AnonymousType Constructors"

    /// <summary>
    /// Constructor for populating via LINQ queries given a LINQ anonymous type
    /// <param name="anonymousType">LINQ anonymous type.</param>
    /// </summary>
    [System.Diagnostics.DebuggerNonUserCode()]
    public Project(object anonymousType) : 
            this()
    {
        foreach (var p in anonymousType.GetType().GetProperties())
        {
            var value = p.GetValue(anonymousType, null);
            var name = p.Name.ToLower();
    
            if (name.EndsWith("enum") && value.GetType().BaseType == typeof(System.Enum))
            {
                value = new Microsoft.Xrm.Sdk.OptionSetValue((int) value);
                name = name.Remove(name.Length - "enum".Length);
            }
    
            switch (name)
            {
                case "id":
                    base.Id = (System.Guid)value;
                    Attributes["projectid"] = base.Id;
                    break;
                case "productid":
                    var id = (System.Nullable<System.Guid>) value;
                    if(id == null){ continue; }
                    base.Id = id.Value;
                    Attributes[name] = base.Id;
                    break;
                case "formattedvalues":
                    // Add Support for FormattedValues
                    FormattedValues.AddRange((Microsoft.Xrm.Sdk.FormattedValueCollection)value);
                    break;
                default:
                    Attributes[name] = value;
                    break;
            }
        }
    }
    

    This then would allow you to change your query to be something like this:

    var projects = (from project in Context.ProjectSet
                   where project.ModifiedOn > DateTime.Now.AddHours(-1)
                   select new Project(new { 
                   {
                       project.Id,
                       project.ModifiedOn,
                       project.CompanylinkId 
                   }).ToList();
    

    This does two great things:

    1. No Need to specify the names of your attributes twice
    2. Now readonly fields are able to be populated, removing the need for your custom attributes!

    You can copy and paste the constructor in if you'd like, switch over to using the EarlyBoundGenerator, or to answer your question more directly as to why your values aren't getting populated, most likely it is because you aren't actually putting the values into the AttributeCollection property of the entity. CRM does some interesting optimizations when converting from Entity to your defined type.