Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should ToEntity<T> Be Used Instead Of A Cast?

The Xrm Sdk defines a ToEntity<T> method. I've always used it to get my early bound entities from CRM. Today I was reviewing some code and saw that the entities were just getting cast:

var contact = service.Retrieve("contact", id, new ColumnSet()) as Contact;

I wasn't even aware of that being possible. Is the ToEntity call even needed anymore?

like image 653
Daryl Avatar asked May 17 '17 12:05

Daryl


2 Answers

Somewhat aside, that's not specifically a cast, its a conversion, and a conversion behaves slightly differently than a straight cast.

As

You can use the as operator to perform certain types of conversions between compatible reference types or nullable types...The as operator is like a cast operation. However, if the conversion isn't possible, as returns null instead of raising an exception.

I'm assuming that your Contact is a class created by CrmSvcUtil e.g. public partial class Contact : Microsoft.Xrm.Sdk.Entity, and service.Retrieve is IOrganizationService.Retrieve which has a return type of Entity.

Contact is a derived class of the base class Entity. You can't cast a base class to a more specific derived class (see Is it possible to assign a base class object to a derived class reference with an explicit typecast in C#?). If you tried to do a cast from Entity to Contact you would get an exception, and a conversion would return a null object.

Example with GeneratedCode from CrmSvcUtil included, but no actual connection to CRM.

var entity = new Entity();

Console.WriteLine($"Type of local entity: {entity.GetType()}");

Console.WriteLine($"Local entity as Contact is null? {entity as Contact == null}");

Output:

Type of local entity: Microsoft.Xrm.Sdk.Entity
Local entity as Contact is null? True

So given Retrieve returns an Entity, which can't be cast to Contact, how does your line of code (var contact = service.Retrieve("contact", id, new ColumnSet()) as Contact;) even work?

Well it's magic. Apparently if you include GeneratedCode from CrmSvcUtil within your application the Retrieve function returns the specific derived classes instead of the generic Entity.

Example with GeneratedCode from CrmSvcUtil included:

CrmServiceClient service = new CrmServiceClient(ConfigurationManager.ConnectionStrings["Crm"].ConnectionString);

Contact c = new Contact()
{
    LastName = "Test"
};

Guid contactId = service.Create(c);

var response = service.Retrieve("contact", contactId, new ColumnSet());

Console.WriteLine($"Type of response from CRM: {response.GetType()}");

Console.WriteLine($"Response from CRM as contact is null? {response as Contact == null}");

Outputs:

Type of response from CRM: Contact
Response from CRM as contact is null? False

Example with no generated code included:

CrmServiceClient service = new CrmServiceClient(ConfigurationManager.ConnectionStrings["Crm"].ConnectionString);

Entity c = new Entity("contact");
c["lastname"] = "Test";

Guid contactId = service.Create(c);

var response = service.Retrieve("contact", contactId, new ColumnSet());

Console.WriteLine($"Type of response: {response.GetType()}");

Outputs:

Type of response: Microsoft.Xrm.Sdk.Entity

Back to your question. If you are including generated code in your project, given that Retrieve is returning a Contact anyway you are fine to just do a simple cast (e.g. (Contact)service.Retrieve(...)) or conversion (as). In terms of what what ToEntity does, it's not actually doing a cast or conversion. It creates a new object and performs a shallow copy among some other things. So use it if meets your need, but you can probably get away without it.

Decomplied code:

public T ToEntity<T>() where T : Entity
{
    if (typeof(T) == typeof(Entity))
    {
        Entity entity = new Entity();
        this.ShallowCopyTo(entity);
        return entity as T;
    }
    if (string.IsNullOrWhiteSpace(this._logicalName))
    {
        throw new NotSupportedException("LogicalName must be set before calling ToEntity()");
    }
    string text = null;
    object[] customAttributes = typeof(T).GetCustomAttributes(typeof(EntityLogicalNameAttribute), true);
    if (customAttributes != null)
    {
        object[] array = customAttributes;
        int num = 0;
        if (num < array.Length)
        {
            EntityLogicalNameAttribute entityLogicalNameAttribute = (EntityLogicalNameAttribute)array[num];
            text = entityLogicalNameAttribute.LogicalName;
        }
    }
    if (string.IsNullOrWhiteSpace(text))
    {
        throw new NotSupportedException("Cannot convert to type that is does not have EntityLogicalNameAttribute");
    }
    if (this._logicalName != text)
    {
        throw new NotSupportedException(string.Format(CultureInfo.InvariantCulture, "Cannot convert entity {0} to {1}", new object[]
        {
            this._logicalName,
            text
        }));
    }
    T t = (T)((object)Activator.CreateInstance(typeof(T)));
    this.ShallowCopyTo(t);
    return t;
}
like image 158
James Wood Avatar answered Nov 20 '22 07:11

James Wood


It always worked like that, have a look at CRM 2011 sample code from here

ColumnSet cols = new ColumnSet(new String[] { "name", "address1_postalcode", "lastusedincampaign", "versionnumber" });
Account retrievedAccount = (Account)_serviceProxy.Retrieve("account", _accountId, cols);
Console.Write("retrieved ");

This is why you have to do EnableProxyTypes(); on your IOrganizationService. Basically if you do that, all calls will return early bound types, not Entity objects (of course early-bounds are inheriting from Entity, but you know what I mean). This is simply a feature regarding getting data from CRM.

This has nothing to do with ToEntity<>(), because you still cannot do something like that:

var account = new Entity("account");
var earlyBoundAccount = account as Account; //this will result in NULL

So if you have Entity (for example in the Plugin Target or PostImage) you will still have to use ToEntity to convert it to an early bound.

UPDATE: I digged deeper and checked what EnableProxyTypes does - it simply uses DataContractSerializerOperationBehavior class to inject it's own IDataContractSurrogate to handle serialization/deserialization of the response (example how it can be used can be found here). By looking into deserialized sources of CRM, you can see for yourself how deserialization is implemented:

object IDataContractSurrogate.GetDeserializedObject(object obj, Type targetType)
{
    bool supportIndividualAssemblies = this._proxyTypesAssembly != null;
    OrganizationResponse organizationResponse = obj as OrganizationResponse;
    if (organizationResponse != null)
    {
        Type typeForName = KnownProxyTypesProvider.GetInstance(supportIndividualAssemblies).GetTypeForName(organizationResponse.ResponseName, this._proxyTypesAssembly);
        if (typeForName == null)
        {
            return obj;
        }
        OrganizationResponse organizationResponse2 = (OrganizationResponse)Activator.CreateInstance(typeForName);
        organizationResponse2.ResponseName = organizationResponse.ResponseName;
        organizationResponse2.Results = organizationResponse.Results;
        return organizationResponse2;
    }
    else
    {
        Entity entity = obj as Entity;
        if (entity == null)
        {
            return obj;
        }
        Type typeForName2 = KnownProxyTypesProvider.GetInstance(supportIndividualAssemblies).GetTypeForName(entity.LogicalName, this._proxyTypesAssembly);
        if (typeForName2 == null)
        {
            return obj;
        }
        Entity entity2 = (Entity)Activator.CreateInstance(typeForName2);
        entity.ShallowCopyTo(entity2);
        return entity2;
    }
}

So basically a type from KnownProxyTypes is obtained via entity logical name and instantiated using Activator. Again - this works only for IOrganizationService for which you enabled proxy types (and as far as I remember, if proxies are in the same assembly IOrganizationService is instantiated, this is enabled by default even if you don't call that explicitly, but this one I'm not 100% sure)

like image 6
Pawel Gradecki Avatar answered Nov 20 '22 09:11

Pawel Gradecki