/// <summary> /// Applies an <see cref="ExpressionExpand"/> on the <see cref="Query{T}"/> to determine what related tables to /// include in the result, any tables touched by the <see cref="ExpressionSelect"/> will have it overriding the <see cref="ExpressionExpand"/> /// </summary> public Query <T> Expand(ExpressionExpand expand) { var clone = Clone(); clone._expand = expand; return(clone); }
/// <summary> /// Backbone for <see cref="CountAsync(CancellationToken, int)"/>, <see cref="ToListAsync(CancellationToken)"/> and <see cref="ToListAndCountAsync(CancellationToken)"/> /// </summary> private async Task <(List <T> result, int count)> ToListAndCountInnerAsync(bool includeCount, int maxCount, CancellationToken cancellation) { var args = await _factory(cancellation); var conn = args.Connection; var sources = args.Sources; var userId = args.UserId; var userToday = args.UserToday; var localizer = args.Localizer; var i = args.Instrumentation; var logger = args.Logger; using var _ = i.Block("Query.ToListAndCountInnerAsync"); IDisposable block; block = i.Block("Get Descriptor"); var resultDesc = TypeDescriptor.Get <T>(); block.Dispose(); block = i.Block("Validate Paths and Props"); _orderby ??= (IsEntityWithKey() ? ExpressionOrderBy.Parse("Id desc") : throw new InvalidOperationException($"Query<{resultDesc.Type.Name}> was executed without an orderby clause")); // Prepare all the query parameters ExpressionSelect selectExp = _select; ExpressionExpand expandExp = _expand; ExpressionOrderBy orderbyExp = _orderby; ExpressionFilter filterExp = _filterConditions?.Aggregate( (e1, e2) => ExpressionFilter.Conjunction(e1, e2)); // To prevent SQL injection ValidatePathsAndProperties(selectExp, expandExp, filterExp, orderbyExp, resultDesc, localizer); // ------------------------ Step #1 block.Dispose(); block = i.Block("Prepare QueryInternals"); // Segment the paths of select and expand along the one-to-many relationships, each one-to-many relationship will // result in a new internal query for the child collection with the original query as its principal query var segments = new Dictionary <ArraySegment <string>, QueryInternal>(new PathEqualityComparer()); // Helper method for creating a an internal query, will be used later in both the select and the expand loops QueryInternal MakeFlatQuery(ArraySegment <string> previousFullPath, ArraySegment <string> subPath, TypeDescriptor desc) { QueryInternal principalQuery = previousFullPath == null ? null : segments[previousFullPath]; ArraySegment <string> pathToCollectionPropertyInPrincipal = previousFullPath == null ? null : subPath; if (principalQuery != null && desc.KeyType == KeyType.None) { // Programmer mistake throw new InvalidOperationException($"[Bug] Type {desc.Name} has no Id property, yet it is used as a navigation collection on another entity"); } string foreignKeyToPrincipalQuery = null; bool isAncestorExpand = false; if (principalQuery != null) { // This loop retrieves the entity descriptor that has the collection property TypeDescriptor collectionPropertyEntity = principalQuery.ResultDescriptor; int i = 0; for (; i < pathToCollectionPropertyInPrincipal.Count - 1; i++) { var step = pathToCollectionPropertyInPrincipal[i]; collectionPropertyEntity = collectionPropertyEntity.NavigationProperty(step).TypeDescriptor; } // Get the collection/Parent property string propertyName = pathToCollectionPropertyInPrincipal[i]; var property = collectionPropertyEntity.Property(propertyName); if (property is NavigationPropertyDescriptor navProperty && navProperty.IsParent) { foreignKeyToPrincipalQuery = "ParentId"; isAncestorExpand = true; } else if (property is CollectionPropertyDescriptor collProperty) { // Must be a collection then foreignKeyToPrincipalQuery = collProperty.ForeignKeyName; } else { throw new InvalidOperationException($"Bug: Segment along a property {property.Name} on type {collectionPropertyEntity.Name} That is neither a collection nor a parent"); } } if (isAncestorExpand) { // the path to parent entity is the path above minus the "Parent" var pathToParentEntity = new ArraySegment <string>( array: pathToCollectionPropertyInPrincipal.Array, offset: 0, count: pathToCollectionPropertyInPrincipal.Count - 1); // Adding this causes the principal query to always include ParentId in the select clause principalQuery.PathsToParentEntitiesWithExpandedAncestors.Add(pathToParentEntity); } // This is the orderby of related queries, and the default orderby of the root query var defaultOrderBy = ExpressionOrderBy.Parse( desc.HasProperty("Index") ? "Index" : desc.HasProperty("SortKey") ? "SortKey" : "Id"); // Prepare the flat query and return it var flatQuery = new QueryInternal { PrincipalQuery = principalQuery, IsAncestorExpand = isAncestorExpand, PathToCollectionPropertyInPrincipal = pathToCollectionPropertyInPrincipal, ForeignKeyToPrincipalQuery = foreignKeyToPrincipalQuery, ResultDescriptor = desc, OrderBy = defaultOrderBy }; return(flatQuery); }
/// <summary> /// A version of <see cref="Expand(ExpressionExpand)" that accepts a string /// </summary> public Query <T> Expand(string expand) { return(Expand(ExpressionExpand.Parse(expand))); }