Example #1
0
        /// <summary>
        /// Retrieves all the data matched by the <see cref="FetchXml"/>
        /// </summary>
        /// <param name="org">The <see cref="IOrganizationService"/> to execute the query against</param>
        /// <param name="metadata">The metadata cache to use when executing the query</param>
        /// <param name="options">The options to apply to the query execution</param>
        /// <returns>The records matched by the query</returns>
        protected EntityCollection RetrieveAll(IOrganizationService org, IAttributeMetadataCache metadata, IQueryExecutionOptions options)
        {
            if (options.Cancelled)
            {
                return(null);
            }

            try
            {
                var res = new EntityCollection(RetrieveSequence(org, metadata, options).ToList());
                res.EntityName = FetchXml.Items.OfType <FetchEntityType>().Single().name;

                return(res);
            }
            catch (Exception ex)
            {
                // Attempt to handle aggregate queries that go over the standard FetchXML limit by rewriting them to retrieve the
                // individual records and apply the aggregation in-memory
                if (!ex.Message.Contains("AggregateQueryRecordLimit"))
                {
                    throw;
                }

                if (AggregateAlternative == null)
                {
                    throw;
                }

                return(AggregateAlternative.RetrieveAll(org, metadata, options));
            }
        }
Example #2
0
        public TableSizeCache(IOrganizationService org, IAttributeMetadataCache metadata)
        {
            _tableSize = new Dictionary <string, int>(StringComparer.OrdinalIgnoreCase);
            _org       = org;
            _metadata  = metadata;

            _version = new Version(((RetrieveVersionResponse)_org.Execute(new RetrieveVersionRequest())).Version);
        }
Example #3
0
        private void FindEntityNameGroupings(IAttributeMetadataCache metadata)
        {
            _entityNameGroupings = new HashSet <string>();

            if (FetchXml.aggregateSpecified && FetchXml.aggregate)
            {
                FindEntityNameGroupings(metadata, Entity.name, Entity.Items);
            }
        }
Example #4
0
        /// <summary>
        /// Converts a FetchXML query to SQL
        /// </summary>
        /// <param name="metadata">The metadata cache to use for the conversion</param>
        /// <param name="fetch">The FetchXML string to convert</param>
        /// <returns>The converted SQL query</returns>
        public static string Convert(IAttributeMetadataCache metadata, string fetch)
        {
            using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(fetch)))
            {
                var serializer = new XmlSerializer(typeof(FetchXml.FetchType));
                var parsed     = (FetchXml.FetchType)serializer.Deserialize(stream);

                return(Convert(metadata, parsed));
            }
        }
Example #5
0
        /// <summary>
        /// Retrieves all the data matched by the <see cref="FetchXml"/>
        /// </summary>
        /// <param name="org">The <see cref="IOrganizationService"/> to execute the query against</param>
        /// <param name="metadata">The metadata cache to use when executing the query</param>
        /// <param name="options">The options to apply to the query execution</param>
        /// <returns>The records matched by the query, with any custom filters, calculated fields and sorted applied</returns>
        protected IEnumerable <Entity> RetrieveSequence(IOrganizationService org, IAttributeMetadataCache metadata, IQueryExecutionOptions options)
        {
            var sequence = RetrieveSequenceInternal(org, metadata, options);

            foreach (var extension in Extensions)
            {
                sequence = extension.ApplyTo(sequence, options);
            }

            return(sequence);
        }
Example #6
0
 /// <summary>
 /// Executes the query
 /// </summary>
 /// <param name="org">The <see cref="IOrganizationService"/> to execute the query against</param>
 /// <param name="metadata">The metadata cache to use when executing the query</param>
 /// <param name="options">The options to apply to the query execution</param>
 /// <remarks>
 /// After calling this method, the results can be retrieved from the <see cref="Result"/> property.
 /// </remarks>
 public void Execute(IOrganizationService org, IAttributeMetadataCache metadata, IQueryExecutionOptions options)
 {
     try
     {
         Result = ExecuteInternal(org, metadata, options);
     }
     catch (Exception ex)
     {
         Result = ex;
     }
 }
Example #7
0
        /// <summary>
        /// Converts a FetchXML &lt;filter&gt; to a SQL condition
        /// </summary>
        /// <param name="metadata">The metadata cache to use for the conversion</param>
        /// <param name="items">The items in the &lt;entity&gt; or &lt;link-entity&gt; to process the &lt;filter&gt; from</param>
        /// <param name="prefix">The alias or name of the table that the &lt;filter&gt; applies to</param>
        /// <param name="aliasToLogicalName">The mapping of table alias to logical name</param>
        /// <returns>The SQL condition equivalent of the &lt;filter&gt; found in the <paramref name="items"/>, or <c>null</c> if no filter was found</returns>
        private static BooleanExpression GetFilter(IAttributeMetadataCache metadata, object[] items, string prefix, IDictionary <string, string> aliasToLogicalName)
        {
            if (items == null)
            {
                return(null);
            }

            var filter = items.OfType <filter>().SingleOrDefault();

            if (filter == null)
            {
                return(null);
            }

            return(GetFilter(metadata, filter, prefix, aliasToLogicalName));
        }
Example #8
0
        protected override Entity[] GetValues(IOrganizationService org, IAttributeMetadataCache metadata, IQueryExecutionOptions options)
        {
            return(Values
                   .Select(dictionary =>
            {
                var entity = new Entity(LogicalName);

                foreach (var attr in dictionary)
                {
                    entity[attr.Key] = attr.Value;
                }

                return entity;
            })
                   .ToArray());
        }
Example #9
0
        /// <summary>
        /// Converts a FetchXML &lt;filter&gt; to a SQL condition
        /// </summary>
        /// <param name="metadata">The metadata cache to use for the conversion</param>
        /// <param name="filter">The FetchXML filter to convert</param>
        /// <param name="prefix">The alias or name of the table that the <paramref name="filter"/> applies to</param>
        /// <param name="aliasToLogicalName">The mapping of table alias to logical name</param>
        /// <returns>The SQL condition equivalent of the <paramref name="filter"/></returns>
        private static BooleanExpression GetFilter(IAttributeMetadataCache metadata, filter filter, string prefix, IDictionary <string, string> aliasToLogicalName)
        {
            BooleanExpression expression = null;
            var type = filter.type == filterType.and ? BooleanBinaryExpressionType.And : BooleanBinaryExpressionType.Or;

            // Convert each <condition> within the filter
            foreach (var condition in filter.Items.OfType <condition>())
            {
                var newExpression = GetCondition(metadata, condition, prefix, aliasToLogicalName);

                if (expression == null)
                {
                    expression = newExpression;
                }
                else
                {
                    expression = new BooleanBinaryExpression
                    {
                        FirstExpression      = expression,
                        BinaryExpressionType = type,
                        SecondExpression     = newExpression
                    }
                };
            }

            // Recurse into sub-<filter>s
            foreach (var subFilter in filter.Items.OfType <filter>())
            {
                var newExpression = GetFilter(metadata, subFilter, prefix, aliasToLogicalName);

                if (expression == null)
                {
                    expression = newExpression;
                }
                else
                {
                    expression = new BooleanBinaryExpression
                    {
                        FirstExpression      = expression,
                        BinaryExpressionType = type,
                        SecondExpression     = newExpression
                    }
                };
            }

            return(expression);
        }
Example #10
0
        protected override Entity[] GetValues(IOrganizationService org, IAttributeMetadataCache metadata, IQueryExecutionOptions options)
        {
            Source.Execute(org, metadata, options);

            if (Source.Result is Exception ex)
            {
                throw ex;
            }

            if (!(Source.Result is EntityCollection entities))
            {
                return(null);
            }

            var converted = new List <Entity>(entities.Entities.Count);

            foreach (var entity in entities.Entities)
            {
                if (options.Cancelled)
                {
                    break;
                }

                var newEntity = new Entity(LogicalName);

                foreach (var attr in Mappings)
                {
                    object value = null;

                    if (entity.Contains(attr.Key))
                    {
                        value = entity[attr.Key];
                    }

                    if (value is Guid g)
                    {
                        value = new EntityReference(entity.LogicalName, g);
                    }

                    newEntity[attr.Value] = value;
                }

                converted.Add(newEntity);
            }

            return(converted.ToArray());
        }
Example #11
0
        /// <inheritdoc/>
        protected override object ExecuteInternal(IOrganizationService org, IAttributeMetadataCache metadata, IQueryExecutionOptions options)
        {
            // Shortcut getting the total number of records in an entity where possible
            if (RetrieveTotalRecordCount(org, metadata, out var result))
            {
                return(result);
            }

            // Run the raw SQL query against the T-SQL endpoint
            if (ExecuteTSQL(org, options, out var dataTable))
            {
                return(dataTable);
            }

            // Execute the FetchXML
            return(RetrieveAll(org, metadata, options));
        }
Example #12
0
        private bool ContainsSortOnLookupAttribute(IAttributeMetadataCache metadata, string logicalName, object[] items, out FetchAttributeType lookupAttr)
        {
            if (items == null)
            {
                lookupAttr = null;
                return(false);
            }

            foreach (var order in items.OfType <FetchOrderType>())
            {
                if (!String.IsNullOrEmpty(order.alias))
                {
                    lookupAttr = items.OfType <FetchAttributeType>().FirstOrDefault(attr => attr.alias.Equals(order.alias, StringComparison.OrdinalIgnoreCase));
                }
                else
                {
                    lookupAttr = items.OfType <FetchAttributeType>().FirstOrDefault(attr => attr.name.Equals(order.attribute, StringComparison.OrdinalIgnoreCase));
                }

                if (lookupAttr == null)
                {
                    continue;
                }

                var meta         = metadata[logicalName];
                var attrName     = lookupAttr.name;
                var attrMetadata = meta.Attributes.SingleOrDefault(a => a.LogicalName.Equals(attrName, StringComparison.OrdinalIgnoreCase));

                if (attrMetadata is LookupAttributeMetadata)
                {
                    return(true);
                }
            }

            foreach (var linkEntity in items.OfType <FetchLinkEntityType>())
            {
                if (ContainsSortOnLookupAttribute(metadata, linkEntity.name, linkEntity.Items, out lookupAttr))
                {
                    return(true);
                }
            }

            lookupAttr = null;
            return(false);
        }
Example #13
0
        public SqlQueryControl(ConnectionDetail con, IAttributeMetadataCache metadata, TelemetryClient ai, Action <WorkAsyncInfo> workAsync, Action <string> setWorkingMessage, Action <Action> executeMethod, Action <MessageBusEventArgs> outgoingMessageHandler, string sourcePlugin)
        {
            InitializeComponent();
            _displayName           = $"Query {++_queryCounter}";
            _modified              = true;
            Service                = con.ServiceClient;
            Metadata               = metadata;
            WorkAsync              = workAsync;
            SetWorkingMessage      = setWorkingMessage;
            ExecuteMethod          = executeMethod;
            OutgoingMessageHandler = outgoingMessageHandler;
            _editor                = CreateSqlEditor();
            _sourcePlugin          = sourcePlugin;
            _ai  = ai;
            _con = con;
            SyncTitle();

            splitContainer.Panel1.Controls.Add(_editor);
            Icon = _sqlIcon;
        }
Example #14
0
        private void FindEntityNameGroupings(IAttributeMetadataCache metadata, string logicalName, object[] items)
        {
            if (items == null)
            {
                return;
            }

            foreach (var attr in items.OfType <FetchAttributeType>().Where(a => a.groupbySpecified && a.groupby == FetchBoolType.@true))
            {
                var attributeMetadata = metadata[logicalName].Attributes.Single(a => a.LogicalName == attr.name);

                if (attributeMetadata.AttributeType == AttributeTypeCode.EntityName)
                {
                    _entityNameGroupings.Add(attr.alias);
                }
            }

            foreach (var linkEntity in items.OfType <FetchLinkEntityType>())
            {
                FindEntityNameGroupings(metadata, linkEntity.name, linkEntity.Items);
            }
        }
Example #15
0
 /// <summary>
 /// Performs the actual query execution. Any exception thrown here will be captured in the <see cref="Result"/> property.
 /// </summary>
 /// <param name="org">The <see cref="IOrganizationService"/> to execute the query against</param>
 /// <param name="metadata">The metadata cache to use when executing the query</param>
 /// <param name="options">The options to apply to the query execution</param>
 protected abstract object ExecuteInternal(IOrganizationService org, IAttributeMetadataCache metadata, IQueryExecutionOptions options);
Example #16
0
        /// <inheritdoc/>
        protected override object ExecuteInternal(IOrganizationService org, IAttributeMetadataCache metadata, IQueryExecutionOptions options)
        {
            // Check if the query is allowed
            if (options.Cancelled)
            {
                return(null);
            }

            if (options.BlockDeleteWithoutWhere && !FetchXml.Items.OfType <FetchEntityType>().Single().Items.OfType <filter>().Any())
            {
                throw new InvalidOperationException("DELETE without WHERE is blocked by your settings");
            }

            var meta = metadata[EntityName];

            // If we are using a bulk delete job, start the job
            if (options.UseBulkDelete && Extensions.Count == 0 && meta.IsIntersect != true)
            {
                var query = ((FetchXmlToQueryExpressionResponse)org.Execute(new FetchXmlToQueryExpressionRequest {
                    FetchXml = Serialize(FetchXml)
                })).Query;

                var bulkDelete = new BulkDeleteRequest
                {
                    JobName               = $"SQL 4 CDS {meta.DisplayCollectionName.UserLocalizedLabel.Label} Bulk Delete Job",
                    QuerySet              = new[] { query },
                    StartDateTime         = DateTime.Now,
                    RunNow                = true,
                    RecurrencePattern     = String.Empty,
                    SendEmailNotification = false,
                    ToRecipients          = new Guid[0],
                    CCRecipients          = new Guid[0]
                };

                org.Execute(bulkDelete);

                return("Bulk delete job started");
            }

            // Otherwise, get the records to delete
            var count    = 0;
            var entities = RetrieveAll(org, metadata, options).Entities;

            if (entities == null)
            {
                return(null);
            }

            // Check again if the query is allowed
            if (!options.ConfirmDelete(entities.Count, meta))
            {
                throw new OperationCanceledException("DELETE cancelled by user");
            }

            ExecuteMultipleRequest multiple = null;

            // Delete hte records in batches
            foreach (var entity in entities)
            {
                if (options.Cancelled)
                {
                    break;
                }

                if (options.BatchSize == 1)
                {
                    options.Progress($"Deleting {meta.DisplayName.UserLocalizedLabel.Label} {count + 1:N0} of {entities.Count:N0}...");
                    org.Execute(CreateDeleteRequest(meta, entity));
                    count++;
                }
                else
                {
                    if (multiple == null)
                    {
                        multiple = new ExecuteMultipleRequest
                        {
                            Requests = new OrganizationRequestCollection(),
                            Settings = new ExecuteMultipleSettings
                            {
                                ContinueOnError = false,
                                ReturnResponses = false
                            }
                        };
                    }

                    multiple.Requests.Add(CreateDeleteRequest(meta, entity));

                    if (multiple.Requests.Count == options.BatchSize)
                    {
                        options.Progress($"Deleting {meta.DisplayCollectionName.UserLocalizedLabel.Label} {count + 1:N0} - {count + multiple.Requests.Count:N0} of {entities.Count:N0}...");
                        var resp = (ExecuteMultipleResponse)org.Execute(multiple);
                        if (resp.IsFaulted)
                        {
                            throw new ApplicationException($"Error deleting {meta.DisplayCollectionName.UserLocalizedLabel.Label}");
                        }

                        count += multiple.Requests.Count;

                        multiple = null;
                    }
                }
            }

            if (!options.Cancelled && multiple != null)
            {
                options.Progress($"Deleting {meta.DisplayCollectionName.UserLocalizedLabel.Label} {count + 1:N0} - {count + multiple.Requests.Count:N0}...");
                var resp = (ExecuteMultipleResponse)org.Execute(multiple);
                if (resp.IsFaulted)
                {
                    throw new ApplicationException($"Error deleting {meta.DisplayCollectionName.UserLocalizedLabel.Label}");
                }

                count += multiple.Requests.Count;
            }

            return($"{count:N0} {meta.DisplayCollectionName.UserLocalizedLabel.Label} deleted");
        }
Example #17
0
        /// <inheritdoc/>
        protected override object ExecuteInternal(IOrganizationService org, IAttributeMetadataCache metadata, IQueryExecutionOptions options)
        {
            if (options.Cancelled)
            {
                return(null);
            }

            // Check if the update is allowed
            if (options.BlockUpdateWithoutWhere && !FetchXml.Items.OfType <FetchEntityType>().Single().Items.OfType <filter>().Any())
            {
                throw new InvalidOperationException("UPDATE without WHERE is blocked by your settings");
            }

            // Get the records to update
            var count    = 0;
            var entities = RetrieveAll(org, metadata, options).Entities;

            if (entities == null)
            {
                return(null);
            }

            var meta = metadata[EntityName];

            // Check again that the update is allowed
            if (!options.ConfirmUpdate(entities.Count, meta))
            {
                throw new OperationCanceledException("UPDATE cancelled by user");
            }

            // Apply the update in batches
            ExecuteMultipleRequest multiple = null;

            foreach (var entity in entities)
            {
                if (options.Cancelled)
                {
                    break;
                }

                var id = entity[IdColumn];
                if (id is AliasedValue alias)
                {
                    id = alias.Value;
                }

                var update = new Entity(EntityName);
                update.Id = (Guid)id;

                foreach (var attr in Updates)
                {
                    update[attr.Key] = attr.Value(entity);
                }

                if (options.BatchSize == 1)
                {
                    options.Progress($"Updating {meta.DisplayName?.UserLocalizedLabel?.Label} {count + 1:N0} of {entities.Count:N0}...");
                    org.Update(update);
                    count++;
                }
                else
                {
                    if (multiple == null)
                    {
                        multiple = new ExecuteMultipleRequest
                        {
                            Requests = new OrganizationRequestCollection(),
                            Settings = new ExecuteMultipleSettings
                            {
                                ContinueOnError = false,
                                ReturnResponses = false
                            }
                        };
                    }

                    multiple.Requests.Add(new UpdateRequest {
                        Target = update
                    });

                    if (multiple.Requests.Count == options.BatchSize)
                    {
                        options.Progress($"Updating {meta.DisplayCollectionName?.UserLocalizedLabel?.Label} {count + 1:N0} - {count + multiple.Requests.Count:N0} of {entities.Count:N0}...");
                        var resp = (ExecuteMultipleResponse)org.Execute(multiple);
                        if (resp.IsFaulted)
                        {
                            throw new ApplicationException($"Error updating {meta.DisplayCollectionName?.UserLocalizedLabel?.Label}");
                        }

                        count += multiple.Requests.Count;

                        multiple = null;
                    }
                }
            }

            if (!options.Cancelled && multiple != null)
            {
                options.Progress($"Updating {meta.DisplayCollectionName?.UserLocalizedLabel?.Label} {count + 1:N0} - {count + multiple.Requests.Count:N0} of {entities.Count:N0}...");
                var resp = (ExecuteMultipleResponse)org.Execute(multiple);
                if (resp.IsFaulted)
                {
                    throw new ApplicationException($"Error updating {meta.DisplayCollectionName?.UserLocalizedLabel?.Label}");
                }

                count += multiple.Requests.Count;
            }

            return($"{count:N0} {meta.DisplayCollectionName?.UserLocalizedLabel?.Label} updated");
        }
Example #18
0
        /// <summary>
        /// Converts a FetchXML query to SQL
        /// </summary>
        /// <param name="metadata">The metadata cache to use for the conversion</param>
        /// <param name="fetch">The query object to convert</param>
        /// <returns>The converted SQL query</returns>
        public static string Convert(IAttributeMetadataCache metadata, FetchXml.FetchType fetch)
        {
            var select = new SelectStatement();
            var query  = new QuerySpecification();

            select.QueryExpression = query;

            if (fetch.top != null)
            {
                query.TopRowFilter = new TopRowFilter {
                    Expression = new IntegerLiteral {
                        Value = fetch.top
                    }
                }
            }
            ;

            if (fetch.distinct)
            {
                query.UniqueRowFilter = UniqueRowFilter.Distinct;
            }

            // SELECT (columns from first table)
            var entity = fetch.Items.OfType <FetchEntityType>().SingleOrDefault();

            AddSelectElements(query, entity.Items, entity?.name);

            // FROM
            var aliasToLogicalName = new Dictionary <string, string>(StringComparer.OrdinalIgnoreCase);

            if (entity != null)
            {
                query.FromClause = new FromClause
                {
                    TableReferences =
                    {
                        new NamedTableReference
                        {
                            SchemaObject = new SchemaObjectName
                            {
                                Identifiers =
                                {
                                    new Identifier {
                                        Value = entity.name
                                    }
                                }
                            }
                        }
                    }
                };

                if (fetch.nolock)
                {
                    ((NamedTableReference)query.FromClause.TableReferences[0]).TableHints.Add(new TableHint {
                        HintKind = TableHintKind.NoLock
                    });
                }

                // Recurse into link-entities to build joins
                query.FromClause.TableReferences[0] = BuildJoins(metadata, query.FromClause.TableReferences[0], (NamedTableReference)query.FromClause.TableReferences[0], entity.Items, query, aliasToLogicalName, fetch.nolock);
            }

            // OFFSET
            if (!String.IsNullOrEmpty(fetch.page) && fetch.page != "1")
            {
                var page     = Int32.Parse(fetch.page);
                var pageSize = Int32.Parse(fetch.count);

                query.OffsetClause = new OffsetClause
                {
                    OffsetExpression = new IntegerLiteral {
                        Value = ((page - 1) * pageSize).ToString()
                    },
                    FetchExpression = new IntegerLiteral {
                        Value = fetch.count
                    }
                };
            }

            // WHERE
            var filter = GetFilter(metadata, entity.Items, entity.name, aliasToLogicalName);

            if (filter != null)
            {
                query.WhereClause = new WhereClause
                {
                    SearchCondition = filter
                };
            }

            // ORDER BY
            AddOrderBy(entity.name, entity.Items, query);

            // For single-table queries, don't bother qualifying the column names to make the query easier to read
            if (query.FromClause.TableReferences[0] is NamedTableReference)
            {
                select.Accept(new SimplifyMultiPartIdentifierVisitor(entity.name));
            }

            // Check whether each identifier needs to be quoted so we have minimal quoting to make the query easier to read
            select.Accept(new QuoteIdentifiersVisitor());

            new Sql150ScriptGenerator().GenerateScript(select, out var sql);

            return(sql);
        }
Example #19
0
        /// <summary>
        /// Get the total number of records in an entity
        /// </summary>
        /// <param name="org">The <see cref="IOrganizationService"/> to execute the query against</param>
        /// <param name="metadata">The metadata cache to use when executing the query</param>
        /// <returns><c>true</c> if this method has retrieved the requested details, or <c>false</c> otherwise</returns>
        private bool RetrieveTotalRecordCount(IOrganizationService org, IAttributeMetadataCache metadata, out EntityCollection result)
        {
            result = null;

            if (FetchXml == null)
            {
                return(false);
            }

            if (Extensions.Count > 0)
            {
                return(false);
            }

            // Special case - SELECT count(primaryid) with no filter
            if (!FetchXml.aggregate)
            {
                return(false);
            }

            var entity     = FetchXml.Items.OfType <FetchEntityType>().Single();
            var attributes = entity.Items.OfType <FetchAttributeType>().ToArray();

            if (attributes.Length != 1 || attributes[0].aggregate != AggregateType.count)
            {
                return(false);
            }

            var filters = entity.Items.OfType <filter>().Count();
            var links   = entity.Items.OfType <FetchLinkEntityType>().Count();

            if (filters != 0 || links != 0)
            {
                return(false);
            }

            if (attributes[0].name != metadata[entity.name].PrimaryIdAttribute)
            {
                return(false);
            }

            // RetrieveTotalRecordCountRequest is only supported in v9+
            var version = (RetrieveVersionResponse)org.Execute(new RetrieveVersionRequest());

            if (!Version.TryParse(version.Version, out var serverVersion) || serverVersion.Major < 9)
            {
                return(false);
            }

            var count = ((RetrieveTotalRecordCountResponse)org.Execute(new RetrieveTotalRecordCountRequest {
                EntityNames = new[] { entity.name }
            })).EntityRecordCountCollection[entity.name];

            var resultEntity = new Entity(entity.name)
            {
                [attributes[0].alias] = new AliasedValue(entity.name, attributes[0].name, count)
            };

            result = new EntityCollection {
                EntityName = entity.name, Entities = { resultEntity }
            };
            return(true);
        }
Example #20
0
        /// <summary>
        /// Recurse through link-entities to add joins to FROM clause and update SELECT clause
        /// </summary>
        /// <param name="metadata">The metadata cache to use for the conversion</param>
        /// <param name="dataSource">The current data source of the SQL query</param>
        /// <param name="parentTable">The details of the table that this new table is being linked to</param>
        /// <param name="items">The FetchXML items in this entity</param>
        /// <param name="query">The current state of the SQL query being built</param>
        /// <param name="aliasToLogicalName">A mapping of table aliases to the logical name</param>
        /// <param name="nolock">Indicates if the NOLOCK table hint should be applied</param>
        /// <returns>The data source including any required joins</returns>
        private static TableReference BuildJoins(IAttributeMetadataCache metadata, TableReference dataSource, NamedTableReference parentTable, object[] items, QuerySpecification query, IDictionary <string, string> aliasToLogicalName, bool nolock)
        {
            if (items == null)
            {
                return(dataSource);
            }

            // Find any <link-entity> elements to process
            foreach (var link in items.OfType <FetchLinkEntityType>())
            {
                // Store the alias of this link
                if (!String.IsNullOrEmpty(link.alias))
                {
                    aliasToLogicalName[link.alias] = link.name;
                }

                // Create the new table reference
                var table = new NamedTableReference
                {
                    SchemaObject = new SchemaObjectName
                    {
                        Identifiers =
                        {
                            new Identifier
                            {
                                Value = link.name
                            }
                        }
                    },
                    Alias = String.IsNullOrEmpty(link.alias) ? null : new Identifier {
                        Value = link.alias
                    }
                };

                if (nolock)
                {
                    table.TableHints.Add(new TableHint {
                        HintKind = TableHintKind.NoLock
                    });
                }

                // Add the join from the current data source to the new table
                var join = new QualifiedJoin
                {
                    FirstTableReference  = dataSource,
                    SecondTableReference = table,
                    QualifiedJoinType    = link.linktype == "outer" ? QualifiedJoinType.LeftOuter : QualifiedJoinType.Inner,
                    SearchCondition      = new BooleanComparisonExpression
                    {
                        FirstExpression = new ColumnReferenceExpression
                        {
                            MultiPartIdentifier = new MultiPartIdentifier
                            {
                                Identifiers =
                                {
                                    new Identifier {
                                        Value = parentTable.Alias?.Value ?? parentTable.SchemaObject.Identifiers.Last().Value
                                    },
                                    new Identifier {
                                        Value = link.to
                                    }
                                }
                            }
                        },
                        ComparisonType   = BooleanComparisonType.Equals,
                        SecondExpression = new ColumnReferenceExpression
                        {
                            MultiPartIdentifier = new MultiPartIdentifier
                            {
                                Identifiers =
                                {
                                    new Identifier {
                                        Value = link.alias ?? link.name
                                    },
                                    new Identifier {
                                        Value = link.from
                                    }
                                }
                            }
                        }
                    }
                };

                // Update the SELECT clause
                AddSelectElements(query, link.Items, link.alias ?? link.name);

                // Handle any filters within the <link-entity> as additional join criteria
                var filter = GetFilter(metadata, link.Items, link.alias ?? link.name, aliasToLogicalName);
                if (filter != null)
                {
                    var finalFilter = new BooleanBinaryExpression
                    {
                        FirstExpression      = join.SearchCondition,
                        BinaryExpressionType = BooleanBinaryExpressionType.And,
                        SecondExpression     = filter
                    };

                    join.SearchCondition = finalFilter;
                }

                // Recurse into any other links
                dataSource = BuildJoins(metadata, join, (NamedTableReference)join.SecondTableReference, link.Items, query, aliasToLogicalName, nolock);
            }

            return(dataSource);
        }
Example #21
0
        /// <summary>
        /// Converts a FetchXML &lt;condition&gt; to a SQL condition
        /// </summary>
        /// <param name="metadata">The metadata cache to use for the conversion</param>
        /// <param name="condition">The FetchXML condition to convert</param>
        /// <param name="prefix">The alias or name of the table that the <paramref name="condition"/> applies to</param>
        /// <param name="aliasToLogicalName">The mapping of table alias to logical name</param>
        /// <returns>The SQL condition equivalent of the <paramref name="condition"/></returns>
        private static BooleanExpression GetCondition(IAttributeMetadataCache metadata, condition condition, string prefix, IDictionary <string, string> aliasToLogicalName)
        {
            // Start with the field reference
            var field = new ColumnReferenceExpression
            {
                MultiPartIdentifier = new MultiPartIdentifier
                {
                    Identifiers =
                    {
                        new Identifier {
                            Value = condition.entityname ?? prefix
                        },
                        new Identifier {
                            Value = condition.attribute
                        }
                    }
                }
            };

            // Get the metadata for the attribute
            BooleanComparisonType type;
            ScalarExpression      value;

            if (!aliasToLogicalName.TryGetValue(condition.entityname ?? prefix, out var logicalName))
            {
                logicalName = condition.entityname ?? prefix;
            }

            var meta = metadata[logicalName];
            var attr = meta.Attributes.SingleOrDefault(a => a.LogicalName == condition.attribute);

            // Get the literal value to compare to
            if (attr == null)
            {
                value = new StringLiteral {
                    Value = condition.value
                }
            }
            ;
            else if (attr.AttributeType == Microsoft.Xrm.Sdk.Metadata.AttributeTypeCode.BigInt ||
                     attr.AttributeType == Microsoft.Xrm.Sdk.Metadata.AttributeTypeCode.Integer ||
                     attr.AttributeType == Microsoft.Xrm.Sdk.Metadata.AttributeTypeCode.Picklist ||
                     attr.AttributeType == Microsoft.Xrm.Sdk.Metadata.AttributeTypeCode.State ||
                     attr.AttributeType == Microsoft.Xrm.Sdk.Metadata.AttributeTypeCode.Status)
            {
                value = new IntegerLiteral {
                    Value = condition.value
                }
            }
            ;
            else if (attr.AttributeType == Microsoft.Xrm.Sdk.Metadata.AttributeTypeCode.Boolean)
            {
                value = new BinaryLiteral {
                    Value = condition.value
                }
            }
            ;
            else if (attr.AttributeType == Microsoft.Xrm.Sdk.Metadata.AttributeTypeCode.Decimal ||
                     attr.AttributeType == Microsoft.Xrm.Sdk.Metadata.AttributeTypeCode.Double)
            {
                value = new NumericLiteral {
                    Value = condition.value
                }
            }
            ;
            else if (attr.AttributeType == Microsoft.Xrm.Sdk.Metadata.AttributeTypeCode.Money)
            {
                value = new MoneyLiteral {
                    Value = condition.value
                }
            }
            ;
            else if (attr.AttributeType == Microsoft.Xrm.Sdk.Metadata.AttributeTypeCode.Lookup ||
                     attr.AttributeType == Microsoft.Xrm.Sdk.Metadata.AttributeTypeCode.Owner ||
                     attr.AttributeType == Microsoft.Xrm.Sdk.Metadata.AttributeTypeCode.Customer)
            {
                value = new IdentifierLiteral {
                    Value = condition.value
                }
            }
            ;
            else
            {
                value = new StringLiteral {
                    Value = condition.value
                }
            };

            // Apply the appropriate conversion for the type of operator
            switch (condition.@operator)
            {
            case @operator.above:
            case @operator.containvalues:
            case @operator.eqbusinessid:
            case @operator.eqorabove:
            case @operator.eqorunder:
            case @operator.equserid:
            case @operator.equserlanguage:
            case @operator.equseroruserhierarchy:
            case @operator.equseroruserhierarchyandteams:
            case @operator.equseroruserteams:
            case @operator.equserteams:
            case @operator.infiscalperiod:
            case @operator.infiscalperiodandyear:
            case @operator.infiscalyear:
            case @operator.inorafterfiscalperiodandyear:
            case @operator.inorbeforefiscalperiodandyear:
            case @operator.lastfiscalperiod:
            case @operator.lastfiscalyear:
            case @operator.lastmonth:
            case @operator.lastsevendays:
            case @operator.lastweek:
            case @operator.lastxdays:
            case @operator.lastxfiscalperiods:
            case @operator.lastxfiscalyears:
            case @operator.lastxhours:
            case @operator.lastxmonths:
            case @operator.lastxweeks:
            case @operator.lastxyears:
            case @operator.lastyear:
            case @operator.nebusinessid:
            case @operator.neuserid:
            case @operator.nextfiscalperiod:
            case @operator.nextfiscalyear:
            case @operator.nextmonth:
            case @operator.nextsevendays:
            case @operator.nextweek:
            case @operator.nextxdays:
            case @operator.nextxfiscalperiods:
            case @operator.nextxfiscalyears:
            case @operator.nextxhours:
            case @operator.nextxmonths:
            case @operator.nextxweeks:
            case @operator.nextxyears:
            case @operator.nextyear:
            case @operator.notcontainvalues:
            case @operator.notunder:
            case @operator.olderthanxdays:
            case @operator.olderthanxhours:
            case @operator.olderthanxminutes:
            case @operator.olderthanxmonths:
            case @operator.olderthanxweeks:
            case @operator.olderthanxyears:
            case @operator.on:
            case @operator.onorafter:
            case @operator.onorbefore:
            case @operator.thisfiscalperiod:
            case @operator.thisfiscalyear:
            case @operator.thismonth:
            case @operator.thisweek:
            case @operator.thisyear:
            case @operator.today:
            case @operator.tomorrow:
            case @operator.under:
            case @operator.yesterday:

                // These FetchXML operators don't have a direct SQL equivalent, so convert to the format
                // field = function(arg)
                // so <condition attribute="createdon" operator="lastxdays" value="2" /> will be converted to
                // createdon = lastxdays(2)

                type  = BooleanComparisonType.Equals;
                value = new FunctionCall {
                    FunctionName = new Identifier {
                        Value = [email protected]()
                    }
                };

                if (condition.value != null)
                {
                    ((FunctionCall)value).Parameters.Add(new StringLiteral {
                        Value = condition.value
                    });
                }

                break;

            case @operator.beginswith:
            case @operator.notbeginwith:
                return(new LikePredicate {
                    FirstExpression = field, SecondExpression = new StringLiteral {
                        Value = condition.value + "%"
                    }, NotDefined = condition.@operator == @operator.notbeginwith
                });

            case @operator.between:
            case @operator.notbetween:
                return(new BooleanTernaryExpression {
                    FirstExpression = field, TernaryExpressionType = condition.@operator == @operator.between ? BooleanTernaryExpressionType.Between : BooleanTernaryExpressionType.NotBetween, SecondExpression = new StringLiteral {
                        Value = condition.Items[0].Value
                    }, ThirdExpression = new StringLiteral {
                        Value = condition.Items[1].Value
                    }
                });

            case @operator.endswith:
            case @operator.notendwith:
                return(new LikePredicate {
                    FirstExpression = field, SecondExpression = new StringLiteral {
                        Value = "%" + condition.value
                    }, NotDefined = condition.@operator == @operator.notendwith
                });

            case @operator.eq:
                type = BooleanComparisonType.Equals;
                break;

            case @operator.ge:
                type = BooleanComparisonType.GreaterThanOrEqualTo;
                break;

            case @operator.gt:
                type = BooleanComparisonType.GreaterThan;
                break;

            case @operator.@in:
            case @operator.notin:
                var @in = new InPredicate {
                    Expression = field, NotDefined = condition.@operator == @operator.notin
                };

                foreach (var val in condition.Items)
                {
                    @in.Values.Add(new StringLiteral {
                        Value = val.Value
                    });
                }

                return(@in);

            case @operator.le:
                type = BooleanComparisonType.LessThanOrEqualTo;
                break;

            case @operator.like:
            case @operator.notlike:
                return(new LikePredicate {
                    FirstExpression = field, SecondExpression = new StringLiteral {
                        Value = condition.value
                    }, NotDefined = condition.@operator == @operator.notlike
                });

            case @operator.lt:
                type = BooleanComparisonType.LessThan;
                break;

            case @operator.ne:
            case @operator.neq:
                type = BooleanComparisonType.NotEqualToBrackets;
                break;

            case @operator.@null:
            case @operator.notnull:
                return(new BooleanIsNullExpression {
                    Expression = field, IsNot = condition.@operator == @operator.notnull
                });

            default:
                throw new NotImplementedException();
            }

            var expression = new BooleanComparisonExpression
            {
                FirstExpression  = field,
                ComparisonType   = type,
                SecondExpression = value
            };

            return(expression);
        }
Example #22
0
 /// <summary>
 /// Creates a new <see cref="MetaMetadataCache"/>
 /// </summary>
 /// <param name="inner">The <see cref="IAttributeMetadataCache"/> that provides the metadata for the standard data entities</param>
 public MetaMetadataCache(IAttributeMetadataCache inner)
 {
     _inner = inner;
 }
Example #23
0
 /// <summary>
 /// Creates a new <see cref="Autocomplete"/>
 /// </summary>
 /// <param name="entities">The list of entities available to use in the query</param>
 /// <param name="metadata">The cache of metadata about each entity</param>
 public Autocomplete(EntityMetadata[] entities, IAttributeMetadataCache metadata)
 {
     _entities = entities;
     _metadata = metadata;
 }
Example #24
0
        internal FetchAttributeType AddAttribute(string colName, Func <FetchAttributeType, bool> predicate, IAttributeMetadataCache metadata, out bool added, out FetchLinkEntityType linkEntity)
        {
            var parts = colName.Split('.');

            if (parts.Length == 1)
            {
                added = false;
                return(Entity.FindAliasedAttribute(colName, predicate, out linkEntity));
            }

            var entityName = parts[0];
            var attr       = new FetchAttributeType {
                name = parts[1].ToLowerInvariant()
            };

            if (Alias == entityName)
            {
                linkEntity = null;

                var meta = metadata[Entity.name].Attributes.SingleOrDefault(a => a.LogicalName == attr.name && a.AttributeOf == null);
                if (meta == null && (attr.name.EndsWith("name") || attr.name.EndsWith("type")))
                {
                    var logicalName = attr.name.Substring(0, attr.name.Length - 4);
                    meta = metadata[Entity.name].Attributes.SingleOrDefault(a => a.LogicalName == logicalName && a.AttributeOf == null);

                    if (meta != null)
                    {
                        attr.name = logicalName;
                    }
                }

                if (Entity.Items != null)
                {
                    var existing = Entity.Items.OfType <FetchAttributeType>().FirstOrDefault(a => a.name == attr.name || a.alias == attr.name);
                    if (existing != null && (predicate == null || predicate(existing)))
                    {
                        added = false;
                        return(existing);
                    }
                }

                Entity.AddItem(attr);
            }
            else
            {
                linkEntity = Entity.FindLinkEntity(entityName);

                var meta = metadata[linkEntity.name].Attributes.SingleOrDefault(a => a.LogicalName == attr.name && a.AttributeOf == null);
                if (meta == null && (attr.name.EndsWith("name") || attr.name.EndsWith("type")))
                {
                    var logicalName = attr.name.Substring(0, attr.name.Length - 4);
                    meta = metadata[linkEntity.name].Attributes.SingleOrDefault(a => a.LogicalName == logicalName && a.AttributeOf == null);

                    if (meta != null)
                    {
                        attr.name = logicalName;
                    }
                }

                if (linkEntity.Items != null)
                {
                    var existing = linkEntity.Items.OfType <FetchAttributeType>().FirstOrDefault(a => a.name == attr.name || a.alias == attr.name);
                    if (existing != null && (predicate == null || predicate(existing)))
                    {
                        added = false;
                        return(existing);
                    }
                }

                linkEntity.AddItem(attr);
            }

            added = true;
            return(attr);
        }
Example #25
0
 /// <summary>
 /// Returns a sequence of the entities to insert
 /// </summary>
 /// <returns></returns>
 protected abstract Entity[] GetValues(IOrganizationService org, IAttributeMetadataCache metadata, IQueryExecutionOptions options);
Example #26
0
        protected override object ExecuteInternal(IOrganizationService org, IAttributeMetadataCache metadata, IQueryExecutionOptions options)
        {
            var meta = metadata[LogicalName];

            // Add each record in turn
            var count    = 0;
            var entities = GetValues(org, metadata, options);

            if (entities != null)
            {
                foreach (var entity in entities)
                {
                    if (options.Cancelled)
                    {
                        break;
                    }

                    // Special cases for intersect entities
                    if (LogicalName == "listmember")
                    {
                        var listId   = entity.GetAttributeValue <EntityReference>("listid");
                        var entityId = entity.GetAttributeValue <EntityReference>("entityid");

                        if (listId == null)
                        {
                            throw new ApplicationException("listid is required");
                        }

                        if (entityId == null)
                        {
                            throw new ApplicationException("entityid is required");
                        }

                        org.Execute(new AddMemberListRequest
                        {
                            ListId   = listId.Id,
                            EntityId = entityId.Id
                        });
                    }
                    else if (meta.IsIntersect == true)
                    {
                        // For generic intersect entities we expect a single many-to-many relationship in the metadata which describes
                        // the relationship that this is the intersect entity for
                        var relationship = meta.ManyToManyRelationships.Single();

                        var entity1 = entity.GetAttributeValue <EntityReference>(relationship.Entity1IntersectAttribute);
                        var entity2 = entity.GetAttributeValue <EntityReference>(relationship.Entity2IntersectAttribute);

                        if (entity1 == null)
                        {
                            throw new ApplicationException($"{relationship.Entity1IntersectAttribute} is required");
                        }

                        if (entity2 == null)
                        {
                            throw new ApplicationException($"{relationship.Entity2IntersectAttribute} is required");
                        }

                        org.Execute(new AssociateRequest
                        {
                            Target       = entity1,
                            Relationship = new Relationship(relationship.SchemaName)
                            {
                                PrimaryEntityRole = EntityRole.Referencing
                            },
                            RelatedEntities = new EntityReferenceCollection(new[] { entity2 })
                        });
                    }
                    else
                    {
                        org.Create(entity);
                    }

                    count++;

                    options.Progress($"Inserted {count:N0} of {entities.Length:N0} {meta.DisplayCollectionName.UserLocalizedLabel.Label} ({(float)count / entities.Length:P0})");
                }
            }

            return($"{entities.Length:N0} {meta.DisplayCollectionName.UserLocalizedLabel.Label} inserted");
        }
Example #27
0
        /// <summary>
        /// Retrieves all the data matched by the <see cref="FetchXml"/>
        /// </summary>
        /// <param name="org">The <see cref="IOrganizationService"/> to execute the query against</param>
        /// <param name="metadata">The metadata cache to use when executing the query</param>
        /// <param name="options">The options to apply to the query execution</param>
        /// <returns>The records matched by the query, with any custom filters and calculated fields applied</returns>
        private IEnumerable <Entity> RetrieveSequenceInternal(IOrganizationService org, IAttributeMetadataCache metadata, IQueryExecutionOptions options)
        {
            if (options.Cancelled)
            {
                yield break;
            }

            var mainEntity = FetchXml.Items.OfType <FetchEntityType>().Single();
            var name       = mainEntity.name;
            var meta       = metadata[name];

            options.Progress($"Retrieving {meta.DisplayCollectionName?.UserLocalizedLabel?.Label}...");

            // Get the first page of results
            var res = org.RetrieveMultiple(new FetchExpression(Serialize(FetchXml)));

            foreach (var entity in res.Entities)
            {
                yield return(entity);
            }

            var count = res.Entities.Count;

            // Aggregate queries return up to 5000 records and don't provide a method to move on to the next page
            // Throw an exception to indicate the error to the caller
            if (AllPages && FetchXml.aggregateSpecified && FetchXml.aggregate && count == 5000 && FetchXml.top != "5000" && !res.MoreRecords)
            {
                throw new ApplicationException("AggregateQueryRecordLimit");
            }

            // Move on to subsequent pages
            while (AllPages && res.MoreRecords && !options.Cancelled && options.ContinueRetrieve(count))
            {
                options.Progress($"Retrieved {count:N0} {meta.DisplayCollectionName?.UserLocalizedLabel?.Label}...");

                if (FetchXml.page == null)
                {
                    FetchXml.page = "2";
                }
                else
                {
                    FetchXml.page = (Int32.Parse(FetchXml.page) + 1).ToString();
                }

                FetchXml.pagingcookie = res.PagingCookie;

                var nextPage = org.RetrieveMultiple(new FetchExpression(Serialize(FetchXml)));

                foreach (var entity in nextPage.Entities)
                {
                    yield return(entity);
                }

                count += nextPage.Entities.Count;
                res    = nextPage;
            }
        }
Example #28
0
        private void AddSchemaAttributes(NodeSchema schema, IAttributeMetadataCache metadata, string entityName, string alias, object[] items)
        {
            if (items == null && !ReturnFullSchema)
            {
                return;
            }

            var meta = metadata[entityName];

            if (ReturnFullSchema)
            {
                foreach (var attrMetadata in meta.Attributes)
                {
                    if (attrMetadata.IsValidForRead == false)
                    {
                        continue;
                    }

                    if (attrMetadata.AttributeOf != null)
                    {
                        continue;
                    }

                    var fullName = $"{alias}.{attrMetadata.LogicalName}";
                    var attrType = attrMetadata.GetAttributeSqlType();
                    AddSchemaAttribute(schema, fullName, attrMetadata.LogicalName, attrType, attrMetadata);
                }
            }

            if (items != null)
            {
                foreach (var attribute in items.OfType <FetchAttributeType>())
                {
                    var attrMetadata = meta.Attributes.Single(a => a.LogicalName == attribute.name);
                    var attrType     = attrMetadata.GetAttributeSqlType();

                    if (attribute.aggregateSpecified && (attribute.aggregate == Engine.FetchXml.AggregateType.count || attribute.aggregate == Engine.FetchXml.AggregateType.countcolumn) ||
                        attribute.dategroupingSpecified)
                    {
                        attrType = typeof(SqlInt32);
                    }

                    string fullName;
                    string attrAlias;

                    if (!String.IsNullOrEmpty(attribute.alias))
                    {
                        if (!FetchXml.aggregate || attribute.groupbySpecified && attribute.groupby == FetchBoolType.@true)
                        {
                            fullName  = $"{alias}.{attribute.alias}";
                            attrAlias = attribute.alias;
                        }
                        else
                        {
                            fullName  = attribute.alias;
                            attrAlias = null;
                        }
                    }
                    else
                    {
                        fullName  = $"{alias}.{attribute.name}";
                        attrAlias = attribute.name;
                    }

                    AddSchemaAttribute(schema, fullName, attrAlias, attrType, attrMetadata);
                }

                if (items.OfType <allattributes>().Any())
                {
                    foreach (var attrMetadata in meta.Attributes)
                    {
                        if (attrMetadata.IsValidForRead == false)
                        {
                            continue;
                        }

                        if (attrMetadata.AttributeOf != null)
                        {
                            continue;
                        }

                        var attrType = attrMetadata.GetAttributeSqlType();
                        var attrName = attrMetadata.LogicalName;
                        var fullName = $"{alias}.{attrName}";

                        AddSchemaAttribute(schema, fullName, attrName, attrType, attrMetadata);
                    }
                }

                foreach (var sort in items.OfType <FetchOrderType>())
                {
                    string fullName;
                    string attributeName;

                    if (!String.IsNullOrEmpty(sort.alias))
                    {
                        var attribute = items.OfType <FetchAttributeType>().SingleOrDefault(a => a.alias.Equals(sort.alias, StringComparison.OrdinalIgnoreCase));

                        if (!FetchXml.aggregate || attribute != null && attribute.groupbySpecified && attribute.groupby == FetchBoolType.@true)
                        {
                            fullName = $"{alias}.{attribute.alias}";
                        }
                        else
                        {
                            fullName = attribute.alias;
                        }

                        attributeName = attribute.name;
                    }
                    else
                    {
                        fullName      = $"{alias}.{sort.attribute}";
                        attributeName = sort.attribute;
                    }

                    // Sorts applied to lookup or enum fields are actually performed on the associated ___name virtual attribute
                    var attrMeta = meta.Attributes.SingleOrDefault(a => a.LogicalName.Equals(attributeName, StringComparison.OrdinalIgnoreCase));

                    if (attrMeta is LookupAttributeMetadata || attrMeta is EnumAttributeMetadata || attrMeta is BooleanAttributeMetadata)
                    {
                        fullName += "name";
                    }

                    schema.SortOrder.Add(fullName);
                }

                foreach (var linkEntity in items.OfType <FetchLinkEntityType>())
                {
                    if (linkEntity.SemiJoin)
                    {
                        continue;
                    }

                    if (schema.PrimaryKey != null)
                    {
                        var childMeta = metadata[linkEntity.name];

                        if (linkEntity.from != childMeta.PrimaryIdAttribute)
                        {
                            if (linkEntity.linktype == "inner")
                            {
                                schema.PrimaryKey = $"{linkEntity.alias}.{childMeta.PrimaryIdAttribute}";
                            }
                            else
                            {
                                schema.PrimaryKey = null;
                            }
                        }
                    }

                    AddSchemaAttributes(schema, metadata, linkEntity.name, linkEntity.alias, linkEntity.Items);
                }
            }
        }
Example #29
0
        private void OnRetrievedEntity(Entity entity, INodeSchema schema, IQueryExecutionOptions options, IAttributeMetadataCache metadata)
        {
            // Expose any formatted values for OptionSetValue and EntityReference values
            foreach (var formatted in entity.FormattedValues)
            {
                if (!entity.Contains(formatted.Key + "name"))
                {
                    entity[formatted.Key + "name"] = formatted.Value;
                }
            }

            if (options.UseLocalTimeZone)
            {
                // For any datetime values, check the metadata to see if they are affected by timezones and convert them
                foreach (var attribute in entity.Attributes.ToList())
                {
                    var entityName    = entity.LogicalName;
                    var attributeName = attribute.Key;
                    var value         = attribute.Value;

                    if (value is AliasedValue alias)
                    {
                        entityName    = alias.EntityLogicalName;
                        attributeName = alias.AttributeLogicalName;
                        value         = alias.Value;
                    }

                    if (value is DateTime dt)
                    {
                        var meta     = metadata[entityName];
                        var attrMeta = (DateTimeAttributeMetadata)meta.Attributes.Single(a => a.LogicalName == attributeName);

                        if (attrMeta.DateTimeBehavior == DateTimeBehavior.UserLocal)
                        {
                            dt = dt.ToLocalTime();
                            entity[attribute.Key] = dt;
                        }
                    }
                }
            }

            // Prefix all attributes of the main entity with the expected alias
            foreach (var attribute in entity.Attributes.Where(attr => !attr.Key.Contains('.') && !(attr.Value is AliasedValue)).ToList())
            {
                entity[$"{Alias}.{attribute.Key}"] = attribute.Value;
            }

            // Only prefix aliased values if they're not aggregates
            PrefixAliasedScalarAttributes(entity, Entity.Items, Alias);

            // Convert aliased values to the underlying value
            foreach (var attribute in entity.Attributes.Where(attr => attr.Value is AliasedValue).ToList())
            {
                var aliasedValue = (AliasedValue)attribute.Value;

                // When grouping by EntityName attributes the value is converted from the normal string value to an OptionSetValue
                // Convert it back now for consistency
                if (_entityNameGroupings.Contains(attribute.Key))
                {
                    int otc;
                    if (aliasedValue.Value is OptionSetValue osv)
                    {
                        otc = osv.Value;
                    }
                    else if (aliasedValue.Value is int i)
                    {
                        otc = i;
                    }
                    else
                    {
                        throw new QueryExecutionException($"Expected ObjectTypeCode value, got {aliasedValue.Value} ({aliasedValue.Value?.GetType()})");
                    }

                    var meta = metadata[otc];
                    entity[attribute.Key] = meta.LogicalName;
                }
                else
                {
                    entity[attribute.Key] = aliasedValue.Value;
                }
            }

            // Copy any grouped values to their full names
            if (FetchXml.aggregateSpecified && FetchXml.aggregate)
            {
                if (Entity.Items != null)
                {
                    foreach (var attr in Entity.Items.OfType <FetchAttributeType>().Where(a => a.groupbySpecified && a.groupby == FetchBoolType.@true))
                    {
                        if (entity.Attributes.TryGetValue(attr.alias, out var value))
                        {
                            entity[$"{Alias}.{attr.alias}"] = value;
                        }
                    }
                }

                foreach (var linkEntity in Entity.GetLinkEntities().Where(le => le.Items != null))
                {
                    foreach (var attr in linkEntity.Items.OfType <FetchAttributeType>().Where(a => a.groupbySpecified && a.groupby == FetchBoolType.@true))
                    {
                        if (entity.Attributes.TryGetValue(attr.alias, out var value))
                        {
                            entity[$"{linkEntity.alias}.{attr.alias}"] = value;
                        }
                    }
                }
            }

            // Expose the type of lookup values
            foreach (var attribute in entity.Attributes.Where(attr => attr.Value is EntityReference).ToList())
            {
                if (!entity.Contains(attribute.Key + "type"))
                {
                    entity[attribute.Key + "type"] = ((EntityReference)attribute.Value).LogicalName;
                }

                //entity[attribute.Key] = ((EntityReference)attribute.Value).Id;
            }

            // Convert values to SQL types
            foreach (var col in schema.Schema)
            {
                object sqlValue;

                if (entity.Attributes.TryGetValue(col.Key, out var value) && value != null)
                {
                    sqlValue = SqlTypeConverter.NetToSqlType(DataSource, value);
                }
                else
                {
                    sqlValue = SqlTypeConverter.GetNullValue(col.Value);
                }

                if (_primaryKeyColumns.TryGetValue(col.Key, out var logicalName) && sqlValue is SqlGuid guid)
                {
                    sqlValue = new SqlEntityReference(DataSource, logicalName, guid);
                }

                entity[col.Key] = sqlValue;
            }
        }