-
Notifications
You must be signed in to change notification settings - Fork 181
Name property not set on EntityReference attribute of retrieved record #555
Comments
Hello @janssen-io, One example where this is working can be found here fake-xrm-easy/FakeXrmEasy.Tests.Shared/FakeContextTests/RetrieveRequestTests/RetrieveRequestTests.cs Lines 19 to 41 in f294b9a
|
@janssen-io as @BetimBeja suggested above, the Name property requires EntityMetadata to be set before it can be returned. |
Thank you for the quick response! I've tried setting the metadata before but both these attempts failed to produce the desired results. Method 1: [Fact]
public void RetrieveEntityReferenceSetsName()
{
// ...
var context = new XrmFakedContext();
var contactMetadata = new EntityMetadata() { LogicalName = "contact" };
contactMetadata.SetSealedPropertyValue("PrimaryNameAttribute", "fullname");
context.InitializeMetadata(contactMetadata);
context.Initialize(new Entity[] { account, contact });
// ...
} Method 2: [Fact]
public void RetrieveEntityReferenceSetsName()
{
// ...
var context = new XrmFakedContext();
context.InitializeMetadata(Assembly.GetAssembly(typeof(Contact)));
context.Initialize(new Entity[] { account, contact });
// ...
} Edit: Edit 2: Seems like method 1 works. :) Too bad method 2 doesn't seem to work for me. Maybe there is something wrong with the early bound entities. Again, thanks for the reply! |
Method 2 was an approximation of Method 1 to save you from having to manually inject metadata, but it won't fit all scenarios. Maybe CrmSvcUtil generates the attribute as you said but it doesn't autopopulate it in the getter method.... so FakeXrmEasy wouldn't know which field to use. Suggest using method 1 for this scenario. |
Closing this one. If you know what caused issue with Method 2 would be good to know though @janssen-io |
I checked if it even created EntityMetadata and I'm happy to report it does. Looking at the method that generates the metadata (MetadataGenerator#L14), I also can't figure out how or even if it gets set via this method. Could it be that the PrimaryNameAttribute does not get set by reading the Early Bound types? I'd be happy to whip up a PR if this is something that should be implemented. My approach would be to look at the generated field Edit: Ah, I see this is not the case for the Early Bounds in the test project of FakeXrmEasy. So probably not the best candidate then... Example of an early bound entity using this tool: [System.Runtime.Serialization.DataContractAttribute()]
[Microsoft.Xrm.Sdk.Client.EntityLogicalNameAttribute("systemuser")]
public partial class SystemUser : Microsoft.Xrm.Sdk.Entity, System.ComponentModel.INotifyPropertyChanging, System.ComponentModel.INotifyPropertyChanged
{
public static class Fields
{
public const string AccessMode = "accessmode";
// etc.
}
/// <summary>
/// Default Constructor.
/// </summary>
[System.Diagnostics.DebuggerNonUserCode()]
public SystemUser() :
base(EntityLogicalName)
{
}
public const string AlternateKeys = "azureactivedirectoryobjectid";
public const string EntityLogicalName = "systemuser";
public const string EntitySchemaName = "SystemUser";
public const string PrimaryIdAttribute = "systemuserid";
public const string PrimaryNameAttribute = "fullname";
public const string EntityLogicalCollectionName = "systemusers";
public const string EntitySetName = "systemusers";
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
public event System.ComponentModel.PropertyChangingEventHandler PropertyChanging;
[System.Diagnostics.DebuggerNonUserCode()]
private void OnPropertyChanged(string propertyName)
{
if ((this.PropertyChanged != null))
{
this.PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
}
[System.Diagnostics.DebuggerNonUserCode()]
private void OnPropertyChanging(string propertyName)
{
if ((this.PropertyChanging != null))
{
this.PropertyChanging(this, new System.ComponentModel.PropertyChangingEventArgs(propertyName));
}
}
/// <summary>
/// Type of user.
/// </summary>
[Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("accessmode")]
public virtual SystemUser_AccessMode? AccessMode
{
[System.Diagnostics.DebuggerNonUserCode()]
get
{
return ((SystemUser_AccessMode?)(EntityOptionSetEnum.GetEnum(this, "accessmode")));
}
[System.Diagnostics.DebuggerNonUserCode()]
set
{
this.OnPropertyChanging("AccessMode");
this.SetAttributeValue("accessmode", value.HasValue ? new Microsoft.Xrm.Sdk.OptionSetValue((int)value) : null);
this.OnPropertyChanged("AccessMode");
}
}
// etc.
/// <summary>
/// Full name of the user.
/// </summary>
[Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("fullname")]
public string FullName
{
[System.Diagnostics.DebuggerNonUserCode()]
get
{
return this.GetAttributeValue<string>("fullname");
}
}
// etc.
/// <summary>
/// 1:N contact_owning_user
/// </summary>
[Microsoft.Xrm.Sdk.RelationshipSchemaNameAttribute("contact_owning_user")]
public System.Collections.Generic.IEnumerable<Crm.Shared.ContractRouting.Contact> contact_owning_user
{
[System.Diagnostics.DebuggerNonUserCode()]
get
{
return this.GetRelatedEntities<Crm.Shared.ContractRouting.Contact>("contact_owning_user", null);
}
[System.Diagnostics.DebuggerNonUserCode()]
set
{
this.OnPropertyChanging("contact_owning_user");
this.SetRelatedEntities<Crm.Shared.ContractRouting.Contact>("contact_owning_user", null, value);
this.OnPropertyChanged("contact_owning_user");
}
}
/// <summary>
/// N:N systemuserroles_association
/// </summary>
[Microsoft.Xrm.Sdk.RelationshipSchemaNameAttribute("systemuserroles_association")]
public System.Collections.Generic.IEnumerable<Crm.Shared.ContractRouting.Role> systemuserroles_association
{
[System.Diagnostics.DebuggerNonUserCode()]
get
{
return this.GetRelatedEntities<Crm.Shared.ContractRouting.Role>("systemuserroles_association", null);
}
[System.Diagnostics.DebuggerNonUserCode()]
set
{
this.OnPropertyChanging("systemuserroles_association");
this.SetRelatedEntities<Crm.Shared.ContractRouting.Role>("systemuserroles_association", null, value);
this.OnPropertyChanged("systemuserroles_association");
}
}
/// <summary>
/// N:1 lk_systemuser_createdonbehalfby
/// </summary>
[Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("createdonbehalfby")]
[Microsoft.Xrm.Sdk.RelationshipSchemaNameAttribute("lk_systemuser_createdonbehalfby", Microsoft.Xrm.Sdk.EntityRole.Referencing)]
public Crm.Shared.ContractRouting.SystemUser Referencinglk_systemuser_createdonbehalfby
{
[System.Diagnostics.DebuggerNonUserCode()]
get
{
return this.GetRelatedEntity<Crm.Shared.ContractRouting.SystemUser>("lk_systemuser_createdonbehalfby", Microsoft.Xrm.Sdk.EntityRole.Referencing);
}
[System.Diagnostics.DebuggerNonUserCode()]
set
{
this.OnPropertyChanging("Referencinglk_systemuser_createdonbehalfby");
this.SetRelatedEntity<Crm.Shared.ContractRouting.SystemUser>("lk_systemuser_createdonbehalfby", Microsoft.Xrm.Sdk.EntityRole.Referencing, value);
this.OnPropertyChanged("Referencinglk_systemuser_createdonbehalfby");
}
}
/// <summary>
/// Constructor for populating via LINQ queries given a LINQ anonymous type
/// <param name="anonymousType">LINQ anonymous type.</param>
/// </summary>
[System.Diagnostics.DebuggerNonUserCode()]
public SystemUser(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["systemuserid"] = base.Id;
break;
case "systemuserid":
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;
}
}
}
} |
Thanks @janssen-io . What version of CrmSvcUtil are you using? Wondering if it's a feature in one of the latest versions as previous ones didn't generate it. If so, it could be added to the method that generates metadata from early bound by using reflection and checking if that attribute exists, and so, use it. The current sample early bound entities in this repo that were generated using v7 doesn't have that attribute for instance. |
I downloaded the latest version of CrmSvcUtil ( On the one hand, the way they add the field is consistent with how other metadata is shown in the generated classes. So it would be nice if FakeXrmEasy could support it. |
Ok, I suppose it would be nice to have a separate method similar to Method 2 with maybe a different name, that does this (i.e. like InitializeMetadataFromEarlyBoundGenerator(Assembly assembly) or maybe with same name but in a different namespace (i.e. FakeXrmEasy.Metadata.Extensions.EarlyBoundGenerator), to make it obvious the 2 methods do slightly different things. |
@jordimontana82 there is this PR #447 which is EarlyBoundGenerator flavored too 😉 |
Since reading a constant value from a class needs to be done using reflection, I propose we use a configuration on what those fields are called. This way the implementation can be generic and anyone can use the EarlyBound flavor they like. |
That sounds like a good way to go. Alternatively we could provide an interface that can be implemented that sets additional metadata by reference on the EntityMetadata created in the MetadataGenerator. That way we don't have to deal with complex configurations and just delegate that responsibility to the client. For common or popular generators we could also provide this interface as part of FakeXrmEasy. Something along the lines of: public static IEnumerable<EntityMetadata> FromEarlyBoundEntities(Assembly earlyBoundEntitiesAssembly, IGenerateMetadata additionalMetadataGenerator = null)
{
List<EntityMetadata> entityMetadatas = new List<EntityMetadata>();
foreach (var earlyBoundEntity in earlyBoundEntitiesAssembly.GetTypes())
{
EntityLogicalNameAttribute entityLogicalNameAttribute = GetCustomAttribute<EntityLogicalNameAttribute>(earlyBoundEntity);
if (entityLogicalNameAttribute == null) continue;
EntityMetadata metadata = new EntityMetadata();
// [...] all the current generator code
if (additionalMetadataGenerator != null)
{
additionalMetadataGenerator.SetMetadata(metadata, earlyBoundEntity);
}
entityMetadatas.Add(metadata);
}
return entityMetadatas;
} |
@BetimBeja I forgot there was such PR , too many PRs 😄 I'd like to keep the 2 methods separate for different reasons:
|
With either of our described approaches, we could introduce them as separate methods. Does either of them seem to fit with the new strategy? Or should we go an alternative route altogether? |
Having separate methods would fit current v1 and next v2 strategy yes. |
While I'm working on a PR, I think just exposing the metadata generator would help a great deal in allowing custom metadata generators. Then one could build their own metadata generator with the newly exposed CrmSvcUtilGenerator as a base implementation and inject the Metadatas using existing methods. Example: // In a developer's own project, they would define this:
public class CustomGenerator()
{
public List<EntityMetadata> GenerateMetadata(Assembly earlyBoundAssembly)
{
// Newly exposed metadata generator that updates the EntityMetadata reference for a single entity Type.
var crmSvcUtilMetaGen = new FakeXrmEasy.Metadata.CrmSvcUtilMetadataGenerator();
List<EntityMetadata> entityMetadatas = new List<EntityMetadata>();
foreach (Type earlyBoundEntity in earlyBoundEntitiesAssembly.GetTypes())
{
EntityLogicalNameAttribute entityLogicalNameAttribute = earlyBoundEntity.GetCustomAttribute<EntityLogicalNameAttribute>();
if (entityLogicalNameAttribute == null) continue;
EntityMetadata metadata = new EntityMetadata();
crmSvcUtilMetaGen.SetMetadata(metadata, earlyBoundEntity, entityLogicalNameAttribute);
// The custom stuff we want to add
SetPrimaryNameAttribute(metadata, earlyBoundEntity);
entityMetadatas.Add(metadata);
}
}
private void SetPrimaryNameAttribute(EntityMetadata metadata, Type earlyBoundEntity)
{
FieldInfo primaryNameAttribute = earlyBoundEntity.GetField("PrimaryNameAttribute", BindingFlags.Static | BindingFlags.Public);
if (primaryNameAttribute != null)
{
metadata.SetFieldValue("_primaryNameAttribute", primaryNameAttribute.GetValue(null));
}
}
}
// And then use it like this:
var metadata = new CustomGenerator().GenerateMetadata(Assembly.GetAssembly(typeof(Contact)));
var context = new XrmFakedContext();
context.InitializeMetadata(metadata); Not sure if this exposes too much of the internals though. What do you think? Edit: considering this change would be small, I'll make a PR to make it clearer. |
I like it |
Thoughts @BetimBeja @bwmodular ? |
Describe the bug
When retrieving a record with an EntityReference column, the
Name
property is not set on the attribute.To Reproduce
Expected behavior
The
Name
property on the retrieved account (accountWithContact
) should be filled with the value of thePrimaryNameAttribute
(FullName
). This is the case on the server (latest Dynamics 365), but not with FakeXrmEasy 1.57.1FakeXrmEasy and Dynamics 365 / CRM version
Dynamics 365 (latest release)
FakeXrmEasy 1.57.1
Screenshots
N/A
Update:
For future readers: the problem is that the official tooling (CrmSvcUtil v9) does not generate the required metadata. Therefore the current version of FakeXrmEasy does also not read it. I expected it to be read as I use a templated early bound generator that does create a field with this metadata.
The text was updated successfully, but these errors were encountered: