private async Task CopyModelAsCsvToStreamAsync(NameValueCollection requestParameters, Stream responseStream, Func <bool> isCancelled, Func <Task> flushResponseAsync = null) { SecurityProviderCache.ValidateCurrentProvider(); string modelName = requestParameters["ModelName"]; string hubName = requestParameters["HubName"]; string filterText = requestParameters["FilterText"]; string sortField = requestParameters["SortField"]; bool sortAscending = requestParameters["SortAscending"].ParseBoolean(); bool showDeleted = requestParameters["ShowDeleted"].ParseBoolean(); string[] parentKeys = requestParameters["ParentKeys"].Split(','); const int PageSize = 250; if (string.IsNullOrEmpty(modelName)) { throw new ArgumentNullException(nameof(modelName), "Cannot download CSV data: no model type name was specified."); } if (string.IsNullOrEmpty(hubName)) { throw new ArgumentNullException(nameof(hubName), "Cannot download CSV data: no hub type name was specified."); } Type modelType = AssemblyInfo.FindType(modelName); if ((object)modelType == null) { throw new InvalidOperationException($"Cannot download CSV data: failed to find model type \"{modelName}\" in loaded assemblies."); } Type hubType = AssemblyInfo.FindType(hubName); if ((object)hubType == null) { throw new InvalidOperationException($"Cannot download CSV data: failed to find hub type \"{hubName}\" in loaded assemblies."); } IRecordOperationsHub hub; // Record operation tuple defines method name and allowed roles Tuple <string, string> queryRecordCountOperation; Tuple <string, string> queryRecordsOperation; string queryRoles; try { hub = Activator.CreateInstance(hubType) as IRecordOperationsHub; if ((object)hub == null) { throw new SecurityException($"Cannot download CSV data: hub type \"{hubName}\" is not a IRecordOperationsHub, access cannot be validated."); } Tuple <string, string>[] recordOperations; try { // Get any authorized query roles as defined in hub records operations for modeled table, default to read allowed for query recordOperations = hub.RecordOperationsCache.GetRecordOperations(modelType); if ((object)recordOperations == null) { throw new NullReferenceException(); } } catch (KeyNotFoundException ex) { throw new SecurityException($"Cannot download CSV data: hub type \"{hubName}\" does not define record operations for \"{modelName}\", access cannot be validated.", ex); } // Get record operation for querying record count queryRecordCountOperation = recordOperations[(int)RecordOperation.QueryRecordCount]; if ((object)queryRecordCountOperation == null) { throw new NullReferenceException(); } // Get record operation for querying records queryRecordsOperation = recordOperations[(int)RecordOperation.QueryRecords]; if ((object)queryRecordsOperation == null) { throw new NullReferenceException(); } // Get any defined role restrictions for record query operation - access to CSV download will based on these roles queryRoles = string.IsNullOrEmpty(queryRecordsOperation.Item1) ? "*" : queryRecordsOperation.Item2 ?? "*"; } catch (Exception ex) { throw new SecurityException($"Cannot download CSV data: failed to instantiate hub type \"{hubName}\" or access record operations, access cannot be validated.", ex); } using (DataContext dataContext = new DataContext()) using (StreamWriter writer = new StreamWriter(responseStream)) { // Validate current user has access to requested data if (!dataContext.UserIsInRole(queryRoles)) { throw new SecurityException($"Cannot download CSV data: access is denied for user \"{Thread.CurrentPrincipal.Identity?.Name ?? "Undefined"}\", minimum required roles = {queryRoles.ToDelimitedString(", ")}."); } AdoDataConnection connection = dataContext.Connection; ITableOperations table = dataContext.Table(modelType); string[] fieldNames = table.GetFieldNames(false); Func <Task> flushAsync = async() => { // ReSharper disable once AccessToDisposedClosure await writer.FlushAsync(); if ((object)flushResponseAsync != null) { await flushResponseAsync(); } }; // Write column headers await writer.WriteLineAsync(string.Join(",", fieldNames.Select(fieldName => connection.EscapeIdentifier(fieldName, true)))); await flushAsync(); // See if modeled table has a flag field that represents a deleted row bool hasDeletedField = !string.IsNullOrEmpty(dataContext.GetIsDeletedFlag(modelType)); // Get query operation methods MethodInfo queryRecordCount = hubType.GetMethod(queryRecordCountOperation.Item1); MethodInfo queryRecords = hubType.GetMethod(queryRecordsOperation.Item1); // Setup query parameters List <object> queryRecordCountParameters = new List <object>(); List <object> queryRecordsParameters = new List <object>(); // Add current show deleted state parameter, if model defines a show deleted field if (hasDeletedField) { queryRecordCountParameters.Add(showDeleted); } // Add any parent key restriction parameters if (parentKeys.Length > 0 && parentKeys[0].Length > 0) { queryRecordCountParameters.AddRange(parentKeys); } // Add parameters for query records from query record count parameters - they match up to this point queryRecordsParameters.AddRange(queryRecordCountParameters); // Add sort field parameter queryRecordsParameters.Add(sortField); // Add ascending sort order parameter queryRecordsParameters.Add(sortAscending); // Track parameter index for current page to query int pageParameterIndex = queryRecordsParameters.Count; // Add page index parameter queryRecordsParameters.Add(0); // Add page size parameter queryRecordsParameters.Add(PageSize); // Add filter text parameter queryRecordCountParameters.Add(filterText); queryRecordsParameters.Add(filterText); // Read queried records in page sets so there is not a memory burden and long initial query delay on very large data sets int recordCount = (int)queryRecordCount.Invoke(hub, queryRecordCountParameters.ToArray()); int totalPages = Math.Max((int)Math.Ceiling(recordCount / (double)PageSize), 1); // Write data pages for (int page = 0; page < totalPages && !isCancelled(); page++) { // Update desired page to query queryRecordsParameters[pageParameterIndex] = page + 1; // Query page records IEnumerable records = queryRecords.Invoke(hub, queryRecordsParameters.ToArray()) as IEnumerable ?? Enumerable.Empty <object>(); int exportCount = 0; // Export page records foreach (object record in records) { // Periodically check for client cancellation if (exportCount++ % (PageSize / 4) == 0 && isCancelled()) { break; } await writer.WriteLineAsync(string.Join(",", fieldNames.Select(fieldName => $"\"{table.GetFieldValue(record, fieldName)}\""))); } await flushAsync(); } } }