using ClosedXML.Excel; using Disco.Data.Repository; using Disco.Models.Exporting; using Disco.Models.Services.Exporting; using Disco.Services.Tasks; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Data; using System.IO; using System.Linq; using System.Linq.Expressions; using System.Text; namespace Disco.Services.Exporting { public static class Exporter { public static ExportResult Export(IExport export, DiscoDataContext database, IScheduledTaskStatus status) where T : IExportOptions, new() where R : IExportRecord { MemoryStream stream; string mimeType; status.UpdateStatus(1, $"Exporting {export.Name}", "Gathering data"); var records = export.BuildRecords(database, status); status.UpdateStatus(70, "Building metadata"); var metadata = export.BuildMetadata(database, records, status); if (metadata.Count == 0) throw new ArgumentException("At least one export field must be specified", nameof(export.Options)); var filenameBuilder = new StringBuilder(); filenameBuilder.Append(export.FilenamePrefix); filenameBuilder.Append('-'); filenameBuilder.Append(status.StartedTimestamp.Value.ToString("yyyyMMdd-HHmmss")); status.UpdateStatus(80, $"Rendering {records.Count} records for export"); switch (export.Options.Format) { case ExportFormat.Csv: filenameBuilder.Append(".csv"); mimeType = "text/csv"; stream = WriteCSV(metadata, records); break; case ExportFormat.Xlsx: filenameBuilder.Append(".xlsx"); mimeType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; stream = WriteXlsx(export.ExcelWorksheetName, export.ExcelTableName, metadata, records); break; default: throw new NotSupportedException($"Unsupported export format: {export.Options.Format}"); } return new ExportResult() { Result = stream, RecordCount = records.Count, Filename = filenameBuilder.ToString(), MimeType = mimeType, }; } private static MemoryStream WriteCSV(List> metadata, List records) where T : IExportRecord { var stream = new MemoryStream(); using (StreamWriter writer = new StreamWriter(stream, Encoding.Default, 0x400, true)) { // Header writer.Write('"'); writer.Write(string.Join("\",\"", metadata.Select(m => m.ColumnName))); writer.Write('"'); // Records foreach (var record in records) { writer.WriteLine(); for (int i = 0; i < metadata.Count; i++) { if (i != 0) { writer.Write(','); } var value = metadata[i].Accessor(record); writer.Write(metadata[i].CsvEncoder(value)); } } } stream.Position = 0; return stream; } private static MemoryStream WriteXlsx(string worksheetName, string tableName, List> metadata, List records) where T : IExportRecord { var stream = new MemoryStream(); // Create DataTable var dataTable = new DataTable(); foreach (var field in metadata) { dataTable.Columns.Add(field.ColumnName, field.ValueType); } foreach (var record in records) { dataTable.Rows.Add(metadata.Select(m => m.Accessor(record)).ToArray()); } using (var xlWorkbook = new XLWorkbook()) { var sheet = xlWorkbook.AddWorksheet(worksheetName); var table = sheet.Cell(1, 1).InsertTable(dataTable, tableName); table.Theme = XLTableTheme.TableStyleMedium2; table.Columns().ForEach(c => c.WorksheetColumn().AdjustToContents(2, 15, 30)); xlWorkbook.SaveAs(stream); } stream.Position = 0; return stream; } public static void Add(this ExportMetadata metadata, Expression> optionAccessor, Func valueAccessor, Func csvValueEncoder = null, string columnName = null) where O : IExportOptions where R : IExportRecord { // is field enabled? if (!optionAccessor.Compile().Invoke(metadata.Options)) return; if (columnName is null) { var member = ((MemberExpression)optionAccessor.Body).Member; var attribute = (DisplayAttribute)member.GetCustomAttributes(typeof(DisplayAttribute), false).Single(); if (metadata.IgnoreGroupNames.Contains(attribute.GroupName)) columnName = attribute.Name; else columnName = $"{attribute.GroupName} {attribute.Name}"; } metadata.Add(columnName, valueAccessor, csvValueEncoder); } public static void Add(this ExportMetadata metadata, string columnName, Func valueAccessor, Func csvValueEncoder = null) where O : IExportOptions where R : IExportRecord { var valueType = typeof(V); if (valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(Nullable<>)) valueType = valueType.GetGenericArguments()[0]; if (csvValueEncoder is null) csvValueEncoder = CsvEncoders.GetEncoder(); var field = new ExportMetadataField(columnName, valueType, (R i) => valueAccessor(i), csvValueEncoder); metadata.Add(field); } public static class CsvEncoders { private static Dictionary> encoders = new Dictionary>() { { typeof(string), StringEncoder }, { typeof(object), ObjectToStringEncoder }, { typeof(byte), ToStringEncoder }, { typeof(byte?), ToStringEncoder }, { typeof(decimal), ToStringEncoder }, { typeof(decimal?), ToStringEncoder }, { typeof(double), ToStringEncoder }, { typeof(double?), ToStringEncoder }, { typeof(float), ToStringEncoder }, { typeof(float?), ToStringEncoder }, { typeof(int), ToStringEncoder }, { typeof(int?), ToStringEncoder }, { typeof(uint), ToStringEncoder }, { typeof(uint?), ToStringEncoder }, { typeof(long), ToStringEncoder }, { typeof(long?), ToStringEncoder }, { typeof(ulong), ToStringEncoder }, { typeof(ulong?), ToStringEncoder }, { typeof(short), ToStringEncoder }, { typeof(short?), ToStringEncoder }, { typeof(ushort), ToStringEncoder }, { typeof(ushort?), ToStringEncoder }, { typeof(bool), ToStringEncoder }, { typeof(bool?), ToStringEncoder }, { typeof(Guid), ToStringEncoder }, { typeof(Guid?), ToStringEncoder }, { typeof(DateTime), DateTimeEncoder }, { typeof(DateTime?), NullableDateTimeEncoder }, }; public static readonly string DateFormat = "yyyy-MM-dd"; public static readonly string DateTimeFormat = DateFormat + " HH:mm:ss"; public static string StringEncoder(object o) => o == null ? null : $"\"{((string)o).Replace("\"", "\"\"")}\""; public static string ObjectToStringEncoder(object o) => o == null ? null : o is string s ? StringEncoder(s) : o.ToString(); public static string ToStringEncoder(object o) => o == null ? null : o.ToString(); public static string CurrencyEncoder(object o) => ((decimal)o).ToString("C"); public static string NullableCurrencyEncoder(object o) => ((decimal?)o).HasValue ? ((decimal?)o).Value.ToString("C") : null; public static string DateEncoder(object o) => ((DateTime)o).ToString(DateFormat); public static string NullableDateEncoder(object o) => ((DateTime?)o).HasValue ? DateEncoder(o) : null; public static string DateTimeEncoder(object o) => ((DateTime)o).ToString(DateTimeFormat); public static string NullableDateTimeEncoder(object o) => ((DateTime?)o).HasValue ? DateTimeEncoder(o) : null; public static Func GetEncoder() => GetEncoder(typeof(T)); public static Func GetEncoder(Type type) { if (encoders.TryGetValue(type, out var encoder)) return encoder; else throw new NotSupportedException($"No encoder for type {type}"); } } } }