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

2

9

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?

Eberhard answered 17/5, 2017 at 12:0 Comment(0)
C
8

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)

Colonist answered 18/5, 2017 at 8:47 Comment(7)
So basically calling enable proxy types calls the toEntity for you, but only for IOrganizationService requests? It doesn't effect the plugin context?Eberhard
it does not call exactly the function ToEntity (you can check my updated answer) but basically it does the same thing under the hood. It does not affect the plugin context, because the only way for this to work would be to inject an early bound entity into the Target - which is not possible as plugin can be registered on many different entities.Colonist
Thanks for the extra details - I hope it's going to help me to work out why upgrading the Microsoft.CrmSdk.XrmTooling assemblies from v 9.0.0.7 to v 9.0.2.4 has meant that a call to CrmServiceClient.RetrieveMultiple seems like it's no longer returning a response using early-bound entity classes. (I don't think anything else has changed... but I'm obviously going to have to check.)Ormsby
Just to update: The latest "Microsoft.CrmSdk.XrmTooling.CoreAssembly is actually 9.0.2.7 and the change that's stopped the ProxyTypes from being returned has happened in that release - the previous ( 9.0.2.5 ) release is still working.Ormsby
I also noticed that, but I hope that this is temporary bug because it will change a lot in development of the tools. I did not find any official Microsoft announcement about this change and I think it should be announced as basically no application using early bound will be working correctly. Also I noticed that XrmTooling is now dependent on Newtonsoft.JSON, so I guess that maybe it's a start of removing old OrganizationService with WebAPI also for IOrganizationService calls... For me it's a bug, it should be at least 9.1 version if it introduces such big changeColonist
Update: The problem is (indirectly) caused by the fact that the default value of the SkipDiscovery connection parameter has changed (from false in 9.0.2.5 to true in 9.0.2.7 ) and, presumably depending on the exact URL you're using, that alters the way the connection is made. If you add ;SkipDiscover=false to the end of your connection URL then you'll likely find everything works again!Ormsby
Oops - it would have been great if I'd written it correctly: ;SkipDiscovery=falseOrmsby
S
9

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;
}
Soften answered 17/5, 2017 at 22:16 Comment(0)
C
8

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)

Colonist answered 18/5, 2017 at 8:47 Comment(7)
So basically calling enable proxy types calls the toEntity for you, but only for IOrganizationService requests? It doesn't effect the plugin context?Eberhard
it does not call exactly the function ToEntity (you can check my updated answer) but basically it does the same thing under the hood. It does not affect the plugin context, because the only way for this to work would be to inject an early bound entity into the Target - which is not possible as plugin can be registered on many different entities.Colonist
Thanks for the extra details - I hope it's going to help me to work out why upgrading the Microsoft.CrmSdk.XrmTooling assemblies from v 9.0.0.7 to v 9.0.2.4 has meant that a call to CrmServiceClient.RetrieveMultiple seems like it's no longer returning a response using early-bound entity classes. (I don't think anything else has changed... but I'm obviously going to have to check.)Ormsby
Just to update: The latest "Microsoft.CrmSdk.XrmTooling.CoreAssembly is actually 9.0.2.7 and the change that's stopped the ProxyTypes from being returned has happened in that release - the previous ( 9.0.2.5 ) release is still working.Ormsby
I also noticed that, but I hope that this is temporary bug because it will change a lot in development of the tools. I did not find any official Microsoft announcement about this change and I think it should be announced as basically no application using early bound will be working correctly. Also I noticed that XrmTooling is now dependent on Newtonsoft.JSON, so I guess that maybe it's a start of removing old OrganizationService with WebAPI also for IOrganizationService calls... For me it's a bug, it should be at least 9.1 version if it introduces such big changeColonist
Update: The problem is (indirectly) caused by the fact that the default value of the SkipDiscovery connection parameter has changed (from false in 9.0.2.5 to true in 9.0.2.7 ) and, presumably depending on the exact URL you're using, that alters the way the connection is made. If you add ;SkipDiscover=false to the end of your connection URL then you'll likely find everything works again!Ormsby
Oops - it would have been great if I'd written it correctly: ;SkipDiscovery=falseOrmsby

© 2022 - 2024 — McMap. All rights reserved.