
In Dynamics 365 Finance and Operations (D365FO), leveraging data contracts with attributes is crucial for defining parameters in the SysOperation framework. However, accessing these attributes dynamically, especially those decorated with custom names using [DataMemberAttribute], can be a challenge. In this article, we’ll explore how to retrieve attribute names dynamically in X++ by utilizing .NET reflection and CLR interop.
Why This Matters
When working with data contracts, it’s common to decorate properties with [DataMemberAttribute] to define custom names. For instance:
[DataContractAttribute]
public class MyContract
{
[DataMemberAttribute('OPRNUM')]
public str parmOperationNumber(str _operationNumber = operationNumber)
{
operationNumber = _operationNumber;
return operationNumber;
}
private str operationNumber;
[DataMemberAttribute('QTY')]
public int parmQuantity(int _quantity = quantity)
{
quantity = _quantity;
return quantity;
}
private int quantity;
}
In this example, we’ve defined custom attribute names, but accessing these names dynamically at runtime requires a different approach.
The Challenge
By default, X++ doesn’t provide a built-in way to dynamically iterate through and access custom attribute names. This can lead to tedious, hard-coded solutions that are prone to errors and maintenance issues.
The Solution: Using .NET Reflection with CLR Interop
The key to solving this challenge is to leverage the powerful .NET reflection capabilities available through CLR interop in X++. Here’s how you can achieve this:
Step-by-Step Solution
- Define the Data Contract: Ensure your contract class and its properties are decorated with
[DataContractAttribute]and[DataMemberAttribute]. - Use CLR Interop to Access Reflection Classes: We’ll utilize
System.TypeandSystem.Reflectionclasses to access the metadata of the contract. - Retrieve Custom Attribute Names: Dynamically extract the custom names defined in
[DataMemberAttribute].
Example Code
Here’s a complete X++ code example to accomplish this:
public void contractToMap(Object _contract)
{
Map contractMap = new Map(Types::String, Types::String);
System.Type clrType;
System.Reflection.PropertyInfo[] properties;
System.Reflection.PropertyInfo property;
System.Object propertyValue;
int i, j;
str propertyName, dataMemberName;
System.Object[] customAttributes;
System.Type dataMemberAttributeType;
System.Reflection.PropertyInfo nameProperty;
System.Attribute attribute;
// Get the CLR type of the contract object
clrType = _contract.GetType();
// Get all public properties of the contract object
properties = clrType.GetProperties();
// Obtain the System.Type of DataMemberAttribute using GetType
dataMemberAttributeType = System.Type::GetType('System.Runtime.Serialization.DataMemberAttribute, System.Runtime.Serialization, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089');
// Iterate over the properties
for (i = 0; i < properties.get_Length(); i++)
{
property = properties.GetValue(i);
// Get the name of the property
propertyName = property.get_Name();
dataMemberName = '';
// Get custom attributes of the property
customAttributes = property.GetCustomAttributes(true);
// Iterate over custom attributes to find DataMemberAttribute
for (j = 0; j < customAttributes.get_Length(); j++)
{
attribute = customAttributes.GetValue(j);
if (attribute.GetType().Equals(dataMemberAttributeType))
{
// Get the 'Name' property of the DataMemberAttribute
nameProperty = dataMemberAttributeType.GetProperty('Name');
dataMemberName = nameProperty.GetValue(attribute, null);
// If 'Name' is not set, it will be empty; in that case, use the property name
if (dataMemberName == '')
{
dataMemberName = propertyName;
}
break; // Exit the loop after finding the DataMemberAttribute
}
}
// If DataMemberAttribute was not found, use the property name
if (dataMemberName == '')
{
dataMemberName = propertyName;
}
// Get the value of the property
propertyValue = property.GetValue(_contract, null);
// Convert null values to empty strings to avoid errors
if (propertyValue == null)
{
propertyValue = '';
}
// Insert the name and value into the map
contractMap.insert(dataMemberName, any2Str(propertyValue));
// Output for verification
info(strFmt("Attribute: %1, Value: %2", dataMemberName, any2Str(propertyValue)));
}
// Now, contractMap contains all the attributes and their values
info(strFmt("The map contains %1 elements.", contractMap.elements()));
}
Explanation
- Obtain the Type of the Contract Object: The
clrTypevariable is used to get the .NET type of the contract object. - Retrieve Properties: We fetch all public properties of the object using reflection.
- Get DataMemberAttribute Type: Using
System.Type::GetType(), we pass the full assembly-qualified name ofDataMemberAttribute. - Iterate Over Properties: For each property, we check for custom attributes and specifically look for
DataMemberAttribute. - Retrieve the Custom Name: If the attribute has a custom
Name, we use it. Otherwise, we fall back to the property name. - Insert into a Map: We insert each attribute name and its corresponding value into a map.
Key Points
- Dynamic Attribute Access: This approach dynamically extracts the custom attribute names without hardcoding them.
- Use of .NET Reflection: By leveraging reflection, you can access properties and their metadata in a flexible way.
- Handling Missing or Null Values: The code handles cases where custom names are not specified or property values are null.
Benefits of This Approach
- Maintainability: If new attributes are added to the contract class, there’s no need to update the iteration logic.
- Flexibility: This solution adapts to changes in attribute names specified in
[DataMemberAttribute]. - Simplicity: Using reflection eliminates the need for manual enumeration or hardcoded attribute names.
Conclusion
By using .NET reflection within X++, you can dynamically access custom attribute names defined with [DataMemberAttribute] in a data contract class. This approach significantly enhances maintainability and flexibility, especially when working with large or frequently changing data contracts.
Final Tips
- Error Handling: Consider adding try-catch blocks to handle potential errors gracefully.
- Performance Considerations: Reflection can impact performance, so use it judiciously in performance-sensitive scenarios.
- Version and Assembly Info: Ensure the assembly version and PublicKeyToken are correctly specified when using
System.Type::GetType().
Example Use Case
Suppose you have a service that processes manufacturing orders. By dynamically accessing attribute names, you can automate the retrieval of data points like “Operation Number” and “Quantity” without needing to hardcode each attribute name. This flexibility allows for easy maintenance as data requirements evolve.
Leave a comment