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();
                    }
                }
        }