/// <summary>
/// Renders the output of a query in a table for a console application
/// </summary>
/// <param name="service">The authenticated IOrganizationService instance to use.</param>
/// <param name="fetchXml">The FetchXml query to use.</param>
static void OutputFetchRequest(IOrganizationService service, string fetchXml)
{
FetchExpression fetchExpression = new(fetchXml);
//Retrieve the data
EntityCollection entityCollection = service.RetrieveMultiple(query: fetchExpression);
// Get column names from the FetchXml
List<string> columns = GetColumns(fetchXml);
// Create the table using https://www.nuget.org/packages/ConsoleTables/2.5.0
var table = new ConsoleTables.ConsoleTable(columns.ToArray());
// Add the rows of the table
entityCollection.Entities.ToList().ForEach(entity =>
{
table.Rows.Add(GetRowValues(columns, entity).ToArray());
});
// Write the table to the console
table.Write();
/// <summary>
/// Get a list of column names from the FetchXml
/// </summary>
/// <param name="fetchXml">The fetchXml query</param>
/// <returns></returns>
static List<string> GetColumns(string fetchXml)
{
XDocument fetchDoc = XDocument.Parse(fetchXml);
XElement fetchElement = fetchDoc.Root;
bool isAggregate = !(fetchElement?.Attributes("aggregate") == null &&
(fetchElement?.Attribute("aggregate")?.Value == "true" ||
fetchElement?.Attribute("aggregate")?.Value == "1"));
// There can only be one entity element
XElement entityElement = fetchElement.Element("entity");
// Get the columns from the entity and any related link-entity elements
return GetColumnsFromElement(element: entityElement, isAggregate: isAggregate);
}
/// <summary>
/// Recursive function to get all column names from an entity or nested link-entity elements
/// </summary>
/// <param name="element">The entity or link-entity element</param>
/// <param name="isAggregate">Whether the query uses aggregation</param>
/// <param name="alias">The alias of the link-entity element</param>
/// <returns></returns>
static List<string> GetColumnsFromElement(XElement element, bool isAggregate, string? alias = null)
{
List<string> columns = new();
// Get the attributes from the element
foreach (XElement attribute in element.Elements("attribute"))
{
StringBuilder sb = new();
// Prepend the alias for non-aggregate link-entities
if (!string.IsNullOrWhiteSpace(alias) && !isAggregate)
{
sb.Append($"{alias}.");
}
// Use the attribute alias if there is one
if (attribute.Attribute("alias") != null)
{
sb.Append(attribute.Attribute("alias")?.Value);
}
else
{
//Use the attribute name
sb.Append(attribute.Attribute("name")?.Value);
}
columns.Add(sb.ToString());
}
// Whether the link-entity intersect attribute is true
bool isIntersect = (element.Attribute("intersect") != null) &&
(element.Attribute("intersect")?.Value == "true" ||
element.Attribute("intersect")?.Value == "1");
// The name of the element
string elementName = element.Attribute("name")?.Value;
// The type of element: 'entity' or 'link-entity'
string elementType = element.Name.LocalName;
// This method requires any non-intersect entity to have attributes
if (columns.Count == 0 && !isIntersect)
{
// An non-intersect element with no attribute elements is technically valid,
// but not supported by this method.
throw new Exception($"No attribute elements in {elementType} element named '{elementName}'.");
}
// Look for any child link-entities
foreach (XElement linkEntity in element.Elements("link-entity"))
{
// Use the alias if any
string? linkEntityName;
if (linkEntity.Attribute("alias") != null)
{
linkEntityName = linkEntity.Attribute("alias")?.Value;
}
else
{
linkEntityName = linkEntity.Attribute("name")?.Value;
}
// Recursive call for nested link-entity elements
columns.AddRange(GetColumnsFromElement(linkEntity, isAggregate, linkEntityName));
}
return columns;
}
/// <summary>
/// Returns the values of a row as strings
/// </summary>
/// <param name="columns">The names of the columns</param>
/// <param name="entity">The entity with the data</param>
/// <returns></returns>
static List<string> GetRowValues(List<string> columns, Entity entity)
{
List<string> values = new();
columns.ForEach(column =>
{
if (entity.Attributes.ContainsKey(column))
{
// Use the formatted value if it available
if (entity.FormattedValues.ContainsKey(column) &&
!string.IsNullOrWhiteSpace(entity.FormattedValues[column]))
{
values.Add($"{entity.FormattedValues[column]}");
}
else
{
// When an alias is used, the Aliased value must be converted
if (entity.Attributes[column] is AliasedValue aliasedValue)
{
// When an EntityReference doesn't have a Name, show the Id
if (aliasedValue.Value is EntityReference lookup &&
string.IsNullOrWhiteSpace(lookup.Name))
{
values.Add($"{lookup.Id:B}");
}
else
{
values.Add($"{aliasedValue.Value}");
}
}
else
{
// Use the simple attribute value
values.Add($"{entity.Attributes[column]}");
}
}
}
// Null values are not in the Attributes collection
else
{
values.Add("NULL");
}
});
return values;
}
}
Add the following using statements at the top of the program.cs file
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System.Text;
using System.Xml.Linq;
Copy and paste the OutputFetchRequest method below the Main method.
Edit the Main method to set your query and use the OutputFetchRequest method.
static void Main(string[] args)
{
using (ServiceClient serviceClient = new(connectionString))
{
if (serviceClient.IsReady)
{
//WhoAmIResponse response =
// (WhoAmIResponse)serviceClient.Execute(new WhoAmIRequest());
//Console.WriteLine("User ID is {0}.", response.UserId);
string fetchQuery = @"<fetch top='5'>
<entity name='account'>
<attribute name='accountclassificationcode' />
<attribute name='createdby' />
<attribute name='createdon' />
<attribute name='name' />
</entity>
</fetch>";
OutputFetchRequest(serviceClient, fetchQuery);
}
else
{
Console.WriteLine(
"A web service connection was not established.");
}
}
// Pause the console so it does not close.
Console.WriteLine("Press the <Enter> key to exit.");
Console.ReadLine();
}
When using C#, you can use the following OutputFetchRequest static method to test FetchXml queries in a console application.
The GetRecords function demonstrates how to compose an HttpRequestMessage to use with an authenticated HttpClient to return a JsonArray containing the requested data.
The OutputFetchRequest method depends on the ConsoleTables NuGet package and requires that all entity or link-entity element attribute elements are included, which is a best practice.
/// <summary>
/// Renders the output of a query in a table for a console application
/// </summary>
/// <param name="client">The authenticated HttpClient instance to use.</param>
/// <param name="entitySetName">The entity set name for the table.</param>
/// <param name="fetchXml">The FetchXml query to use.</param>
private static async Task OutputFetchRequest(HttpClient client, string entitySetName, string fetchXml)
{
// Retrieve the data from Dataverse
JsonArray records = await GetRecords(client, entitySetName, fetchXml);
// Get column names from the FetchXml
List<string> columns = GetColumns(fetchXml);
// Create the table using https://www.nuget.org/packages/ConsoleTables/2.5.0
var table = new ConsoleTables.ConsoleTable(columns.ToArray());
foreach (JsonObject record in records.Cast<JsonObject>())
{
table.Rows.Add(GetRowValues(columns, record).ToArray());
}
// Write the table to the console
table.Write();
/// <summary>
/// Retrieves records from Dataverse
/// </summary>
/// <param name="client">The authenticated HttpClient instance to use.</param>
/// <param name="entitySetName">The entity set name for the table.</param>
/// <param name="fetchXml">The FetchXml query to use.</param>
/// <returns>JsonArray of records</returns>
static async Task<JsonArray> GetRecords(HttpClient client, string entitySetName, string fetchXml)
{
// Prepare the request
HttpRequestMessage request = new()
{
Method = HttpMethod.Get,
RequestUri = new Uri(
uriString: $"{entitySetName}?fetchXml={WebUtility.UrlEncode(fetchXml.ToString())}",
uriKind: UriKind.Relative),
};
// Add annotations to return formatted values
request.Headers.Add("Prefer", "odata.include-annotations=\"*\"");
// Send the request
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
string jsonContent = await response.Content.ReadAsStringAsync();
// Using System.Text.Json
JsonObject content = JsonNode.Parse(jsonContent)?.AsObject();
// Records are in the value property
content.TryGetPropertyValue("value", out JsonNode records);
return records.AsArray();
}
else
{
throw new Exception($"Web API call failed. Status Code: {response.StatusCode}");
}
}
/// <summary>
/// Get a list of column names from the FetchXml
/// </summary>
/// <param name="fetchXml">The fetchXml query</param>
/// <returns>List of column names</returns>
static List<string> GetColumns(string fetchXml)
{
XDocument fetchDoc = XDocument.Parse(fetchXml);
XElement fetchElement = fetchDoc.Root;
bool isAggregate = !(fetchElement?.Attributes("aggregate") == null &&
(fetchElement?.Attribute("aggregate")?.Value == "true" ||
fetchElement?.Attribute("aggregate")?.Value == "1"));
// There can only be one entity element
XElement entityElement = fetchElement.Element("entity");
// Get the columns from the entity and any related link-entity elements
List<string> columns = GetColumnsFromElement(element: entityElement, isAggregate: isAggregate);
return columns;
}
/// <summary>
/// Recursive function to get all column names from an entity or nested link-entity elements
/// </summary>
/// <param name="element">The entity or link-entity element</param>
/// <param name="alias">The alias of the link-entity element</param>
/// <returns></returns>
static List<string> GetColumnsFromElement(XElement element, bool isAggregate, string? alias = null)
{
List<string> columns = new();
// Get the attributes from the element
foreach (XElement attribute in element.Elements("attribute"))
{
StringBuilder sb = new();
// Prepend the alias for non-aggregate link-entities
if (!string.IsNullOrWhiteSpace(alias) && !isAggregate)
{
sb.Append($"{alias}.");
}
// Use the attribute alias if there is one
if (attribute.Attribute("alias") != null)
{
sb.Append(attribute.Attribute("alias")?.Value);
}
else
{
//Use the attribute name
sb.Append(attribute.Attribute("name")?.Value);
}
columns.Add(sb.ToString());
}
// Whether the link-entity intersect attribute is true
bool isIntersect = (element.Attribute("intersect") != null) &&
(element.Attribute("intersect")?.Value == "true" ||
element.Attribute("intersect")?.Value == "1");
// The name of the element
string elementName = element.Attribute("name")?.Value;
// The type of element: 'entity' or 'link-entity'
string elementType = element.Name.LocalName;
// This method requires any non-intersect entity to have attributes
if (columns.Count == 0 && !isIntersect)
{
// An non-intersect element with no attribute elements is technically valid,
// but not supported by this method.
throw new Exception($"No attribute elements in {elementType} element named '{elementName}'.");
}
// Look for any child link-entities
foreach (XElement linkEntity in element.Elements("link-entity"))
{
// Use the alias if any
string? linkEntityName;
if (linkEntity.Attribute("alias") != null)
{
linkEntityName = linkEntity.Attribute("alias")?.Value;
}
else
{
linkEntityName = linkEntity.Attribute("name")?.Value;
}
// Recursive call for nested link-entity elements
columns.AddRange(GetColumnsFromElement(linkEntity, isAggregate, linkEntityName));
}
return columns;
}
/// <summary>
/// Returns the values of a row as strings
/// </summary>
/// <param name="columns">The names of the columns</param>
/// <param name="record">The record with the data</param>
/// <returns></returns>
static List<string> GetRowValues(List<string> columns, JsonObject record)
{
List<string> values = new();
columns.ForEach(column =>
{
string lookupPropertyName = $"_{column}_value";
bool isLookup = record.ContainsKey(lookupPropertyName);
if (record.ContainsKey(column) || isLookup)
{
string formattedValueKey = string.Format("{0}@OData.Community.Display.V1.FormattedValue",
isLookup ? lookupPropertyName : column);
// Use the formatted value if it available and visible
if (record.ContainsKey(formattedValueKey) &&
!string.IsNullOrWhiteSpace((string)record[formattedValueKey]))
{
values.Add($"{record[formattedValueKey]}");
}
else
{
// Use the simple property value
values.Add($"{record[column]}");
}
}
// Null values are not returned
else
{
values.Add("NULL");
}
});
return values;
}
}