diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/SaveResultsRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/SaveResultsRequest.cs
index 517bfe88..5f79112b 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/SaveResultsRequest.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/Contracts/SaveResultsRequest.cs
@@ -80,6 +80,17 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
public bool IncludeHeaders { get; set; }
}
+ ///
+ /// Parameters to save results as Excel
+ ///
+ public class SaveResultsAsExcelRequestParams : SaveResultsRequestParams
+ {
+ ///
+ /// Include headers of columns in Excel
+ ///
+ public bool IncludeHeaders { get; set; }
+ }
+
///
/// Parameters to save results as JSON
///
@@ -120,6 +131,16 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts
RequestType.Create("query/saveCsv");
}
+ ///
+ /// Request type to save results as Excel
+ ///
+ public class SaveResultsAsExcelRequest
+ {
+ public static readonly
+ RequestType Type =
+ RequestType.Create("query/saveExcel");
+ }
+
///
/// Request type to save results as JSON
///
diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/SaveAsExcelFileStreamFactory.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/SaveAsExcelFileStreamFactory.cs
new file mode 100644
index 00000000..d8a1423a
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/SaveAsExcelFileStreamFactory.cs
@@ -0,0 +1,72 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using System;
+using System.IO;
+using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
+using Microsoft.SqlTools.ServiceLayer.SqlContext;
+
+namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
+{
+ ///
+ /// Factory for creating a reader/writer pair that will read from the temporary buffer file
+ /// and output to a Excel file.
+ ///
+ public class SaveAsExcelFileStreamFactory : IFileStreamFactory
+ {
+ #region Properties
+
+ ///
+ /// Settings for query execution
+ ///
+ public QueryExecutionSettings QueryExecutionSettings { get; set; }
+
+ ///
+ /// Parameters for the save as Excel request
+ ///
+ public SaveResultsAsExcelRequestParams SaveRequestParams { get; set; }
+
+ #endregion
+
+ ///
+ /// File names are not meant to be created with this factory.
+ ///
+ /// Thrown all times
+ [Obsolete]
+ public string CreateFile()
+ {
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// Returns a new service buffer reader for reading results back in from the temporary buffer files
+ ///
+ /// Path to the temp buffer file
+ /// Stream reader
+ public IFileStreamReader GetReader(string fileName)
+ {
+ return new ServiceBufferFileStreamReader(new FileStream(fileName, FileMode.Open, FileAccess.Read), QueryExecutionSettings);
+ }
+
+ ///
+ /// Returns a new Excel writer for writing results to a Excel file
+ ///
+ /// Path to the Excel output file
+ /// Stream writer
+ public IFileStreamWriter GetWriter(string fileName)
+ {
+ return new SaveAsExcelFileStreamWriter(new FileStream(fileName, FileMode.Create, FileAccess.ReadWrite), SaveRequestParams);
+ }
+
+ ///
+ /// Safely deletes the file
+ ///
+ /// Path to the file to delete
+ public void DisposeFile(string fileName)
+ {
+ FileUtilities.SafeFileDelete(fileName);
+ }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/SaveAsExcelFileStreamWriter.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/SaveAsExcelFileStreamWriter.cs
new file mode 100644
index 00000000..f6ac59e5
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/SaveAsExcelFileStreamWriter.cs
@@ -0,0 +1,90 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
+
+namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
+{
+ ///
+ /// Writer for writing rows of results to a Excel file
+ ///
+ public class SaveAsExcelFileStreamWriter : SaveAsStreamWriter
+ {
+
+ #region Member Variables
+
+ private readonly SaveResultsAsExcelRequestParams saveParams;
+ private bool headerWritten;
+ private SaveAsExcelFileStreamWriterHelper helper;
+ private SaveAsExcelFileStreamWriterHelper.ExcelSheet sheet;
+
+ #endregion
+
+ ///
+ /// Constructor, stores the Excel specific request params locally, chains into the base
+ /// constructor
+ ///
+ /// FileStream to access the Excel file output
+ /// Excel save as request parameters
+ public SaveAsExcelFileStreamWriter(Stream stream, SaveResultsAsExcelRequestParams requestParams)
+ : base(stream, requestParams)
+ {
+ saveParams = requestParams;
+ helper = new SaveAsExcelFileStreamWriterHelper(stream);
+ sheet = helper.AddSheet();
+ }
+
+ ///
+ /// Writes a row of data as a Excel row. If this is the first row and the user has requested
+ /// it, the headers for the column will be emitted as well.
+ ///
+ /// The data of the row to output to the file
+ ///
+ /// The entire list of columns for the result set. They will be filtered down as per the
+ /// request params.
+ ///
+ public override void WriteRow(IList row, IList columns)
+ {
+ int columnStart = ColumnStartIndex ?? 0;
+ int columnEnd = (ColumnEndIndex != null) ? ColumnEndIndex.Value + 1 : columns.Count;
+
+ // Write out the header if we haven't already and the user chose to have it
+ if (saveParams.IncludeHeaders && !headerWritten)
+ {
+ sheet.AddRow();
+ for (int i = columnStart; i < columnEnd; i++)
+ {
+ sheet.AddCell(columns[i].ColumnName);
+ }
+ headerWritten = true;
+ }
+
+ sheet.AddRow();
+ for (int i = columnStart; i < columnEnd; i++)
+ {
+ sheet.AddCell(row[i]);
+ }
+ }
+
+ private bool disposed;
+ override protected void Dispose(bool disposing)
+ {
+ if (disposed)
+ return;
+
+ sheet.Dispose();
+ helper.Dispose();
+
+ disposed = true;
+ base.Dispose(disposing);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/SaveAsExcelFileStreamWriterHelper.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/SaveAsExcelFileStreamWriterHelper.cs
new file mode 100644
index 00000000..e276f559
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/SaveAsExcelFileStreamWriterHelper.cs
@@ -0,0 +1,805 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Xml;
+
+namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
+{
+ // A xlsx file is a zip with specific folder structure.
+ // http://www.ecma-international.org/publications/standards/Ecma-376.htm
+
+ // The page number in the comments are based on
+ // ECMA-376, Fifth Edition, Part 1 - Fundamentals And Markup Language Reference
+
+ // Page 75, SpreadsheetML package structure
+ // |- [Content_Types].xml
+ // |- _rels
+ // |- .rels
+ // |- xl
+ // |- workbook.xml
+ // |- styles.xml
+ // |- _rels
+ // |- workbook.xml.rels
+ // |- worksheets
+ // |- sheet1.xml
+
+ ///
+ /// A helper class for write xlsx file base on ECMA-376. It tries to be minimal,
+ /// both in implementation and runtime allocation.
+ ///
+ ///
+ /// This sample shows how to use the class
+ ///
+ /// public class TestClass
+ /// {
+ /// public static int Main()
+ /// {
+ /// using (Stream stream = File.Create("test.xlsx"))
+ /// using (var helper = new SaveAsExcelFileStreamWriterHelper(stream, false))
+ /// using (var sheet = helper.AddSheet())
+ /// {
+ /// sheet.AddRow();
+ /// sheet.AddCell("string");
+ /// }
+ /// }
+ /// }
+ ///
+ ///
+
+ internal sealed class SaveAsExcelFileStreamWriterHelper : IDisposable
+ {
+ ///
+ /// Present a Excel sheet
+ ///
+ public sealed class ExcelSheet : IDisposable
+ {
+ // The excel epoch is 1/1/1900, but it has 1/0/1900 and 2/29/1900
+ // which is equal to set the epoch back two days to 12/30/1899
+ // new DateTime(1899,12,30).Ticks
+ private const long ExcelEpochTick = 599264352000000000L;
+
+ // Excel can not use date before 1/0/1900 and
+ // date before 3/1/1900 is wrong, off by 1 because of 2/29/1900
+ // thus, for any date before 3/1/1900, use string for date
+ // new DateTime(1900,3,1).Ticks
+ private const long ExcelDateCutoffTick = 599317056000000000L;
+
+ // new TimeSpan(24,0,0).Ticks
+ private const long TicksPerDay = 864000000000L;
+
+ private XmlWriter writer;
+ private ReferenceManager referenceManager;
+ private bool hasOpenRowTag;
+
+ ///
+ /// Initializes a new instance of the ExcelSheet class.
+ ///
+ /// XmlWriter to write the sheet data
+ internal ExcelSheet(XmlWriter writer)
+ {
+ this.writer = writer;
+ writer.WriteStartDocument();
+ writer.WriteStartElement("worksheet", "http://schemas.openxmlformats.org/spreadsheetml/2006/main");
+ writer.WriteAttributeString("xmlns", "r", null, "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
+ writer.WriteStartElement("sheetData");
+ referenceManager = new ReferenceManager(writer);
+ }
+
+ ///
+ /// Start a new row
+ ///
+ public void AddRow()
+ {
+ EndRowIfNeeded();
+ hasOpenRowTag = true;
+
+ referenceManager.AssureRowReference();
+
+ writer.WriteStartElement("row");
+ referenceManager.WriteAndIncreaseRowReference();
+ }
+
+ ///
+ /// Write a string cell
+ ///
+ /// string value to write
+ public void AddCell(string value)
+ {
+ // string needs string
+ // This class uses inlineStr instead of more common shared string table
+ // to improve write performance and reduce implementation complexity
+ referenceManager.AssureColumnReference();
+ if (value == null)
+ {
+ AddCellEmpty();
+ return;
+ }
+
+ writer.WriteStartElement("c");
+
+ referenceManager.WriteAndIncreaseColumnReference();
+
+ writer.WriteAttributeString("t", "inlineStr");
+
+ writer.WriteStartElement("is");
+ writer.WriteStartElement("t");
+ writer.WriteValue(value);
+ writer.WriteEndElement();
+ writer.WriteEndElement();
+
+ writer.WriteEndElement();
+ }
+
+ ///
+ /// Write a object cell
+ ///
+ /// The program will try to output number/datetime, otherwise, call the ToString
+ ///
+ public void AddCell(DbCellValue dbCellValue)
+ {
+ object o = dbCellValue.RawObject;
+ if (dbCellValue.IsNull || o == null)
+ {
+ AddCellEmpty();
+ return;
+ }
+ switch (Type.GetTypeCode(o.GetType()))
+ {
+ case TypeCode.Boolean:
+ AddCell((bool)o);
+ break;
+ case TypeCode.Byte:
+ case TypeCode.Int16:
+ case TypeCode.Int32:
+ case TypeCode.Int64:
+ case TypeCode.Single:
+ case TypeCode.Double:
+ case TypeCode.Decimal:
+ AddCellBoxedNumber(o);
+ break;
+ case TypeCode.DateTime:
+ AddCell((DateTime)o);
+ break;
+ case TypeCode.String:
+ AddCell((string)o);
+ break;
+ default:
+ if (o is TimeSpan) //TimeSpan doesn't have TypeCode
+ {
+ AddCell((TimeSpan)o);
+ break;
+ }
+ AddCell(dbCellValue.DisplayValue);
+ break;
+ }
+ }
+
+ ///
+ /// Close the tags and close the stream
+ ///
+ public void Dispose()
+ {
+ EndRowIfNeeded();
+ writer.WriteEndElement(); //
+ writer.WriteEndElement(); //
+ writer.Dispose();
+ }
+
+ ///
+ /// Write a empty cell
+ ///
+ /// This only increases the internal bookmark and doesn't arcturally write out anything.
+ private void AddCellEmpty()
+ {
+ referenceManager.IncreaseColumnReference();
+ }
+
+ ///
+ /// Write a bool cell.
+ ///
+ ///
+ private void AddCell(bool value)
+ {
+ // Excel FALSE: 0
+ // Excel TRUE: 1
+ referenceManager.AssureColumnReference();
+
+ writer.WriteStartElement("c");
+
+ referenceManager.WriteAndIncreaseColumnReference();
+
+ writer.WriteAttributeString("t", "b");
+
+ writer.WriteStartElement("v");
+ if (value)
+ {
+ writer.WriteValue("1"); //use string to avoid convert
+ }
+ else
+ {
+ writer.WriteValue("0");
+ }
+ writer.WriteEndElement();
+
+ writer.WriteEndElement();
+ }
+
+ ///
+ /// Write a TimeSpan cell.
+ ///
+ ///
+ private void AddCell(TimeSpan time)
+ {
+ referenceManager.AssureColumnReference();
+ double excelDate = (double)time.Ticks / (double)TicksPerDay;
+ // The default hh:mm:ss format do not support more than 24 hours
+ // For that case, use the format string [h]:mm:ss
+ if (time.Ticks >= TicksPerDay)
+ {
+ AddCellDateTimeInternal(excelDate, Style.TimeMoreThan24Hours);
+ }
+ else
+ {
+ AddCellDateTimeInternal(excelDate, Style.Time);
+ }
+ }
+
+ ///
+ /// Write a DateTime cell.
+ ///
+ /// Datetime
+ ///
+ /// If the DateTime does not have date part, it will be written as datetime and show as time only
+ /// If the DateTime is before 1900-03-01, save as string because excel doesn't support them.
+ /// Otherwise, save as datetime, and if the time is 00:00:00, show as yyyy-MM-dd.
+ /// Show the datetime as yyyy-MM-dd HH:mm:ss if none of the previous situations
+ ///
+ private void AddCell(DateTime dateTime)
+ {
+ referenceManager.AssureColumnReference();
+ long ticks = dateTime.Ticks;
+ Style style = Style.DateTime;
+ double excelDate;
+ if (ticks < TicksPerDay) //date empty, time only
+ {
+ style = Style.Time;
+ excelDate = ((double)ticks) / (double)TicksPerDay;
+ }
+ else if (ticks < ExcelDateCutoffTick) //before excel cut-off, use string
+ {
+ AddCell(dateTime.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture));
+ return;
+ }
+ else
+ {
+ if (ticks % TicksPerDay == 0) //time empty, date only
+ {
+ style = Style.Date;
+ }
+ excelDate = ((double)(ticks - ExcelEpochTick)) / (double)TicksPerDay;
+ }
+ AddCellDateTimeInternal(excelDate, style);
+ }
+
+ // number needs 12.5
+ private void AddCellBoxedNumber(object number)
+ {
+ referenceManager.AssureColumnReference();
+
+ writer.WriteStartElement("c");
+
+ referenceManager.WriteAndIncreaseColumnReference();
+
+ writer.WriteStartElement("v");
+ writer.WriteValue(number);
+ writer.WriteEndElement();
+
+ writer.WriteEndElement();
+ }
+
+
+ // datetime needs 26012.451
+ private void AddCellDateTimeInternal(double excelDate, Style style)
+ {
+ writer.WriteStartElement("c");
+
+ referenceManager.WriteAndIncreaseColumnReference();
+
+ writer.WriteStartAttribute("s");
+ writer.WriteValue((int)style);
+ writer.WriteEndAttribute();
+
+ writer.WriteStartElement("v");
+ writer.WriteValue(excelDate);
+ writer.WriteEndElement();
+
+ writer.WriteEndElement();
+ }
+
+ private void EndRowIfNeeded()
+ {
+ if (hasOpenRowTag)
+ {
+ writer.WriteEndElement(); //
+ }
+ }
+
+
+ }
+
+ ///
+ /// Helper class to track the current cell reference.
+ ///
+ ///
+ /// SpreadsheetML cell needs a reference attribute. (e.g. r="A1"). This class is used
+ /// to track the current cell reference.
+ ///
+ internal class ReferenceManager
+ {
+ private int currColumn; // 0 is invalid, the first AddRow will set to 1
+ private int currRow = 1;
+
+ // In order to reduce allocation, current reference is saved in this array,
+ // and write to the XmlWriter through WriteChars.
+ // For example, when the reference has value AA15,
+ // The content of this array will be @AA15xxxxx, with currReferenceRowLength=2
+ // and currReferenceColumnLength=2
+ private char[] currReference = new char[3 + 7]; //maximal XFD1048576
+ private int currReferenceRowLength;
+ private int currReferenceColumnLength;
+
+ private XmlWriter writer;
+
+ ///
+ /// Initializes a new instance of the ReferenceManager class.
+ ///
+ /// XmlWriter to write the reference attribute to.
+ public ReferenceManager(XmlWriter writer)
+ {
+ this.writer = writer;
+ }
+
+ ///
+ /// Check that we have not write too many columns. (xlsx has a limit of 16384 columns)
+ ///
+ public void AssureColumnReference()
+ {
+ if (currColumn == 0)
+ {
+ throw new InvalidOperationException("AddRow must be called before AddCell");
+
+ }
+ if (currColumn > 16384)
+ {
+ throw new InvalidOperationException("max column number is 16384, see https://support.office.com/en-us/article/Excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3");
+ }
+ }
+
+ ///
+ /// Write out the r="A1" attribute and increase the column number of internal bookmark
+ ///
+ public void WriteAndIncreaseColumnReference()
+ {
+ writer.WriteStartAttribute("r");
+ writer.WriteChars(currReference, 3 - currReferenceColumnLength, currReferenceRowLength + currReferenceColumnLength);
+ writer.WriteEndAttribute();
+ IncreaseColumnReference();
+ }
+
+ ///
+ /// Increase the column of internal bookmark.
+ ///
+ public void IncreaseColumnReference()
+ {
+ // This function change the first three chars of currReference array
+ // The logic is simple, when a start a new row, the array is reset to @@A
+ // where @='A'-1. At each increase, check if the current reference is Z
+ // and move to AA if needed, since the maximal is 16384, or XFD, the code
+ // manipulates the array element directly instead of loop
+ char[] reference = currReference;
+ currColumn++;
+ if ('Z' == reference[2]++)
+ {
+ reference[2] = 'A';
+ if (currReferenceColumnLength < 2)
+ {
+ currReferenceColumnLength = 2;
+ }
+ if ('Z' == reference[1]++)
+ {
+ reference[0]++;
+ reference[1] = 'A';
+ currReferenceColumnLength = 3;
+ }
+ }
+ }
+
+ ///
+ /// Check that we have not write too many rows. (xlsx has a limit of 1048576 rows)
+ ///
+ public void AssureRowReference()
+ {
+ if (currRow > 1048576)
+ {
+ throw new InvalidOperationException("max row number is 1048576, see https://support.office.com/en-us/article/Excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3");
+ }
+ }
+ ///
+ /// Write out the r="1" attribute and increase the row number of internal bookmark
+ ///
+ public void WriteAndIncreaseRowReference()
+ {
+ writer.WriteStartAttribute("r");
+ writer.WriteValue(currRow);
+ writer.WriteEndAttribute();
+
+ ResetColumnReference(); //This need to be called before the increase
+
+ currRow++;
+ }
+
+ // Reset the Column Reference
+ // This will reset the first three chars of currReference array to '@@A'
+ // and the rest to the array to the string presentation of the current row.
+ private void ResetColumnReference()
+ {
+ currColumn = 1;
+ currReference[0] = currReference[1] = (char)('A' - 1);
+ currReference[2] = 'A';
+ currReferenceColumnLength = 1;
+
+ string rowReference = XmlConvert.ToString(currRow);
+ currReferenceRowLength = rowReference.Length;
+ rowReference.CopyTo(0, currReference, 3, rowReference.Length);
+ }
+ }
+
+ private enum Style
+ {
+ Normal = 0,
+ Date = 1,
+ Time = 2,
+ DateTime = 3,
+ TimeMoreThan24Hours = 4,
+ }
+
+ private ZipArchive zipArchive;
+ private List sheetNames = new List();
+ private XmlWriterSettings writerSetting = new XmlWriterSettings()
+ {
+ CloseOutput = true,
+ };
+
+ ///
+ /// Initializes a new instance of the SaveAsExcelFileStreamWriterHelper class.
+ ///
+ /// The input or output stream.
+ public SaveAsExcelFileStreamWriterHelper(Stream stream)
+ {
+ zipArchive = new ZipArchive(stream, ZipArchiveMode.Create, false);
+ }
+
+ ///
+ /// Initializes a new instance of the SaveAsExcelFileStreamWriterHelper class.
+ ///
+ /// The input or output stream.
+ /// true to leave the stream open after the
+ /// SaveAsExcelFileStreamWriterHelper object is disposed; otherwise, false.
+ public SaveAsExcelFileStreamWriterHelper(Stream stream, bool leaveOpen)
+ {
+ zipArchive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen);
+ }
+
+ ///
+ /// Add sheet inside the Xlsx file.
+ ///
+ /// Sheet name
+ /// ExcelSheet for writing the sheet content
+ ///
+ /// When the sheetName is null, sheet1,shhet2,..., will be used.
+ /// The following charactors are not allowed in the sheetName
+ /// '\', '/','*','[',']',':','?'
+ ///
+ public ExcelSheet AddSheet(string sheetName = null)
+ {
+ string sheetFileName = "sheet" + (sheetNames.Count + 1);
+ if (sheetName == null)
+ {
+ sheetName = sheetFileName;
+ }
+ EnsureValidSheetName(sheetName);
+
+ sheetNames.Add(sheetName);
+ XmlWriter sheetWriter = AddEntry($"xl/worksheets/{sheetFileName}.xml");
+ return new ExcelSheet(sheetWriter);
+ }
+
+ ///
+ /// Write out the rest of the xlsx files and release the resources used by the current instance
+ ///
+ public void Dispose()
+ {
+ WriteMinimalTemplate();
+ zipArchive.Dispose();
+ }
+
+
+ private XmlWriter AddEntry(string entryName)
+ {
+ ZipArchiveEntry entry = zipArchive.CreateEntry(entryName, CompressionLevel.Fastest);
+ return XmlWriter.Create(entry.Open(), writerSetting);
+ }
+
+ //ECMA-376 page 75
+ private void WriteMinimalTemplate()
+ {
+ WriteTopRel();
+ WriteWorkbook();
+ WriteStyle();
+ WriteContentType();
+ WriteWorkbookRel();
+ }
+
+ ///
+ /// write [Content_Types].xml
+ ///
+ ///
+ /// This file need to describe all the files in the zip.
+ ///
+ private void WriteContentType()
+ {
+ using (XmlWriter xw = AddEntry("[Content_Types].xml"))
+ {
+ xw.WriteStartDocument();
+ xw.WriteStartElement("Types", "http://schemas.openxmlformats.org/package/2006/content-types");
+
+ xw.WriteStartElement("Default");
+ xw.WriteAttributeString("Extension", "rels");
+ xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-package.relationships+xml");
+ xw.WriteEndElement();
+
+ xw.WriteStartElement("Override");
+ xw.WriteAttributeString("PartName", "/xl/workbook.xml");
+ xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml");
+ xw.WriteEndElement();
+
+ xw.WriteStartElement("Override");
+ xw.WriteAttributeString("PartName", "/xl/styles.xml");
+ xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml");
+ xw.WriteEndElement();
+
+ for (int i = 1; i <= sheetNames.Count; ++i)
+ {
+ xw.WriteStartElement("Override");
+ xw.WriteAttributeString("PartName", "/xl/worksheets/sheet" + i + ".xml");
+ xw.WriteAttributeString("ContentType", "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml");
+ xw.WriteEndElement();
+ }
+ xw.WriteEndElement();
+ xw.WriteEndDocument();
+ }
+ }
+
+ ///
+ /// Write _rels/.rels. This file only need to reference main workbook
+ ///
+ private void WriteTopRel()
+ {
+ using (XmlWriter xw = AddEntry("_rels/.rels"))
+ {
+ xw.WriteStartDocument();
+
+ xw.WriteStartElement("Relationships", "http://schemas.openxmlformats.org/package/2006/relationships");
+
+ xw.WriteStartElement("Relationship");
+ xw.WriteAttributeString("Id", "rId1");
+ xw.WriteAttributeString("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument");
+ xw.WriteAttributeString("Target", "xl/workbook.xml");
+ xw.WriteEndElement();
+
+ xw.WriteEndElement();
+
+ xw.WriteEndDocument();
+ }
+ }
+
+ private static char[] invalidSheetNameCharacters = new char[]
+ {
+ '\\', '/','*','[',']',':','?'
+ };
+ private void EnsureValidSheetName(string sheetName)
+ {
+ if (sheetName.IndexOfAny(invalidSheetNameCharacters) != -1)
+ {
+ throw new ArgumentException($"Invalid sheetname: sheetName");
+ }
+ if (sheetNames.IndexOf(sheetName) != -1)
+ {
+ throw new ArgumentException($"Duplicate sheetName: {sheetName}");
+ }
+ }
+
+ ///
+ /// Write xl/workbook.xml. This file will references the sheets through ids in xl/_rels/workbook.xml.rels
+ ///
+ private void WriteWorkbook()
+ {
+ using (XmlWriter xw = AddEntry("xl/workbook.xml"))
+ {
+ xw.WriteStartDocument();
+ xw.WriteStartElement("workbook", "http://schemas.openxmlformats.org/spreadsheetml/2006/main");
+ xw.WriteAttributeString("xmlns", "r", null, "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
+ xw.WriteStartElement("sheets");
+ for (int i = 1; i <= sheetNames.Count; i++)
+ {
+ xw.WriteStartElement("sheet");
+ xw.WriteAttributeString("name", sheetNames[i - 1]);
+ xw.WriteAttributeString("sheetId", i.ToString());
+ xw.WriteAttributeString("r", "id", null, "rId" + i);
+ xw.WriteEndElement();
+ }
+ xw.WriteEndDocument();
+ }
+ }
+
+ ///
+ /// Write xl/_rels/workbook.xml.rels. This file will have the paths of the style and sheets.
+ ///
+ private void WriteWorkbookRel()
+ {
+ using (XmlWriter xw = AddEntry("xl/_rels/workbook.xml.rels"))
+ {
+ xw.WriteStartDocument();
+ xw.WriteStartElement("Relationships", "http://schemas.openxmlformats.org/package/2006/relationships");
+
+ xw.WriteStartElement("Relationship");
+ xw.WriteAttributeString("Id", "rId0");
+ xw.WriteAttributeString("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles");
+ xw.WriteAttributeString("Target", "styles.xml");
+ xw.WriteEndElement();
+
+ for (int i = 1; i <= sheetNames.Count; i++)
+ {
+ xw.WriteStartElement("Relationship");
+ xw.WriteAttributeString("Id", "rId" + i);
+ xw.WriteAttributeString("Type", "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet");
+ xw.WriteAttributeString("Target", "worksheets/sheet" + i + ".xml");
+ xw.WriteEndElement();
+ }
+ xw.WriteEndElement();
+ xw.WriteEndDocument();
+ }
+ }
+
+ // Write the xl/styles.xml
+ private void WriteStyle()
+ {
+ // the style 0 is used for general case, style 1 for date, style 2 for time and style 3 for datetime see Enum Style
+ // reference chain: (index start with 0)
+ // (in sheet1.xml) --> (by s) --> (by xfId)
+ // --> (by numFmtId)
+ // that is will reference the second element of
+ // then, this xf reference numFmt by name and get formatCode "hh:mm:ss"
+
+ using (XmlWriter xw = AddEntry("xl/styles.xml"))
+ {
+ xw.WriteStartElement("styleSheet", "http://schemas.openxmlformats.org/spreadsheetml/2006/main");
+
+ xw.WriteStartElement("numFmts");
+ xw.WriteAttributeString("count", "4");
+ xw.WriteStartElement("numFmt");
+ xw.WriteAttributeString("numFmtId", "166");
+ xw.WriteAttributeString("formatCode", "yyyy-mm-dd");
+ xw.WriteEndElement();
+ xw.WriteStartElement("numFmt");
+ xw.WriteAttributeString("numFmtId", "167");
+ xw.WriteAttributeString("formatCode", "hh:mm:ss");
+ xw.WriteEndElement();
+ xw.WriteStartElement("numFmt");
+ xw.WriteAttributeString("numFmtId", "168");
+ xw.WriteAttributeString("formatCode", "yyyy-mm-dd hh:mm:ss");
+ xw.WriteEndElement();
+ xw.WriteStartElement("numFmt");
+ xw.WriteAttributeString("numFmtId", "169");
+ xw.WriteAttributeString("formatCode", "[h]:mm:ss");
+ xw.WriteEndElement();
+ xw.WriteEndElement(); //mumFmts
+
+
+ xw.WriteStartElement("fonts");
+ xw.WriteAttributeString("count", "1");
+ xw.WriteStartElement("font");
+ xw.WriteStartElement("sz");
+ xw.WriteAttributeString("val", "11");
+ xw.WriteEndElement();
+ xw.WriteStartElement("color");
+ xw.WriteAttributeString("theme", "1");
+ xw.WriteEndElement();
+ xw.WriteStartElement("name");
+ xw.WriteAttributeString("val", "Calibri");
+ xw.WriteEndElement();
+ xw.WriteStartElement("family");
+ xw.WriteAttributeString("val", "2");
+ xw.WriteEndElement();
+ xw.WriteStartElement("scheme");
+ xw.WriteAttributeString("val", "minor");
+ xw.WriteEndElement();
+ xw.WriteEndElement(); // font
+ xw.WriteEndElement(); // fonts
+
+ xw.WriteStartElement("fills");
+ xw.WriteAttributeString("count", "1");
+ xw.WriteStartElement("fill");
+ xw.WriteStartElement("patternFill");
+ xw.WriteAttributeString("patternType", "none");
+ xw.WriteEndElement(); // patternFill
+ xw.WriteEndElement(); // fill
+ xw.WriteEndElement(); // fills
+
+ xw.WriteStartElement("borders");
+ xw.WriteAttributeString("count", "1");
+ xw.WriteStartElement("border");
+ xw.WriteElementString("left", null);
+ xw.WriteElementString("right", null);
+ xw.WriteElementString("top", null);
+ xw.WriteElementString("bottom", null);
+ xw.WriteElementString("diagonal", null);
+ xw.WriteEndElement(); // board
+ xw.WriteEndElement(); // borders
+
+ xw.WriteStartElement("cellStyleXfs");
+ xw.WriteAttributeString("count", "1");
+ xw.WriteStartElement("xf");
+ xw.WriteAttributeString("numFmtId", "0");
+ xw.WriteAttributeString("fontId", "0");
+ xw.WriteAttributeString("fillId", "0");
+ xw.WriteAttributeString("borderId", "0");
+ xw.WriteEndElement(); // xf
+ xw.WriteEndElement(); // cellStyleXfs
+
+ xw.WriteStartElement("cellXfs");
+ xw.WriteAttributeString("count", "5");
+ xw.WriteStartElement("xf");
+ xw.WriteAttributeString("xfId", "0");
+ xw.WriteEndElement();
+ xw.WriteStartElement("xf");
+ xw.WriteAttributeString("numFmtId", "166");
+ xw.WriteAttributeString("xfId", "0");
+ xw.WriteAttributeString("applyNumberFormat", "1");
+ xw.WriteEndElement();
+ xw.WriteStartElement("xf");
+ xw.WriteAttributeString("numFmtId", "167");
+ xw.WriteAttributeString("xfId", "0");
+ xw.WriteAttributeString("applyNumberFormat", "1");
+ xw.WriteEndElement();
+ xw.WriteStartElement("xf");
+ xw.WriteAttributeString("numFmtId", "168");
+ xw.WriteAttributeString("xfId", "0");
+ xw.WriteAttributeString("applyNumberFormat", "1");
+ xw.WriteEndElement();
+ xw.WriteStartElement("xf");
+ xw.WriteAttributeString("numFmtId", "169");
+ xw.WriteAttributeString("xfId", "0");
+ xw.WriteAttributeString("applyNumberFormat", "1");
+ xw.WriteEndElement();
+ xw.WriteEndElement(); // cellXfs
+
+ xw.WriteStartElement("cellStyles");
+ xw.WriteAttributeString("count", "1");
+ xw.WriteStartElement("cellStyle");
+ xw.WriteAttributeString("name", "Normal");
+ xw.WriteAttributeString("builtinId", "0");
+ xw.WriteAttributeString("xfId", "0");
+ xw.WriteEndElement(); // cellStyle
+ xw.WriteEndElement(); // cellStyles
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/SaveAsJsonFileStreamWriter.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/SaveAsJsonFileStreamWriter.cs
index cd9fdc79..e13f42db 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/SaveAsJsonFileStreamWriter.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/SaveAsJsonFileStreamWriter.cs
@@ -57,10 +57,10 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
{
// Write the header for the object
jsonWriter.WriteStartObject();
-
+
// Write the items out as properties
int columnStart = ColumnStartIndex ?? 0;
- int columnEnd = (ColumnEndIndex != null) ? ColumnEndIndex.Value + 1 : columns.Count;
+ int columnEnd = (ColumnEndIndex != null) ? ColumnEndIndex.Value + 1 : columns.Count;
for (int i = columnStart; i < columnEnd; i++)
{
jsonWriter.WritePropertyName(columns[i].ColumnName);
@@ -78,16 +78,24 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
jsonWriter.WriteEndObject();
}
+ private bool disposed = false;
///
/// Disposes the writer by closing up the array that contains the row objects
///
- public new void Dispose()
+ protected override void Dispose(bool disposing)
{
- // Write the footer of the file
- jsonWriter.WriteEndArray();
- // This closes the underlying stream, so we needn't call close on the underlying stream explicitly
- jsonWriter.Close();
- base.Dispose();
+ if (disposed)
+ return;
+
+ if (disposing)
+ {
+ // Write the footer of the file
+ jsonWriter.WriteEndArray();
+ // This closes the underlying stream, so we needn't call close on the underlying stream explicitly
+ jsonWriter.Close();
+ }
+ disposed = true;
+ base.Dispose(disposing);
}
}
}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/SaveAsWriterBase.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/SaveAsWriterBase.cs
index 1c70e51b..12ff6b46 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/SaveAsWriterBase.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/DataStorage/SaveAsWriterBase.cs
@@ -100,17 +100,19 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
/// Disposes the instance by flushing and closing the file stream
///
///
- private void Dispose(bool disposing)
+ protected virtual void Dispose(bool disposing)
{
- if (disposed || !disposing)
- {
- disposed = true;
+ if (disposed)
return;
- }
- FileStream.Dispose();
+ if (disposing)
+ {
+ FileStream.Dispose();
+ }
+ disposed = true;
}
- public virtual void Dispose()
+
+ public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
diff --git a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs
index 3829f5a2..a9f0a012 100644
--- a/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/QueryExecution/QueryExecutionService.cs
@@ -83,6 +83,12 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
///
internal IFileStreamFactory CsvFileFactory { get; set; }
+ ///
+ /// File factory to be used to create Excel files from result sets. Set to internal in order
+ /// to allow overriding in unit testing
+ ///
+ internal IFileStreamFactory ExcelFileFactory { get; set; }
+
///
/// File factory to be used to create JSON files from result sets. Set to internal in order
/// to allow overriding in unit testing
@@ -128,6 +134,7 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
serviceHost.SetRequestHandler(QueryDisposeRequest.Type, HandleDisposeRequest);
serviceHost.SetRequestHandler(QueryCancelRequest.Type, HandleCancelRequest);
serviceHost.SetRequestHandler(SaveResultsAsCsvRequest.Type, HandleSaveResultsAsCsvRequest);
+ serviceHost.SetRequestHandler(SaveResultsAsExcelRequest.Type, HandleSaveResultsAsExcelRequest);
serviceHost.SetRequestHandler(SaveResultsAsJsonRequest.Type, HandleSaveResultsAsJsonRequest);
serviceHost.SetRequestHandler(QueryExecutionPlanRequest.Type, HandleExecutionPlanRequest);
@@ -308,6 +315,21 @@ namespace Microsoft.SqlTools.ServiceLayer.QueryExecution
await SaveResultsHelper(saveParams, requestContext, csvFactory);
}
+ ///
+ /// Process request to save a resultSet to a file in Excel format
+ ///
+ internal async Task HandleSaveResultsAsExcelRequest(SaveResultsAsExcelRequestParams saveParams,
+ RequestContext requestContext)
+ {
+ // Use the default Excel file factory if we haven't overridden it
+ IFileStreamFactory excelFactory = ExcelFileFactory ?? new SaveAsExcelFileStreamFactory
+ {
+ SaveRequestParams = saveParams,
+ QueryExecutionSettings = Settings.QueryExecutionSettings
+ };
+ await SaveResultsHelper(saveParams, requestContext, excelFactory);
+ }
+
///
/// Process request to save a resultSet to a file in JSON format
///
diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestData/DataStorage/SaveAsExcelFileStreamWriterHelperTests/[Content_Types].xml b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestData/DataStorage/SaveAsExcelFileStreamWriterHelperTests/[Content_Types].xml
new file mode 100644
index 00000000..f4d8e54f
--- /dev/null
+++ b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestData/DataStorage/SaveAsExcelFileStreamWriterHelperTests/[Content_Types].xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestData/DataStorage/SaveAsExcelFileStreamWriterHelperTests/_rels/.rels b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestData/DataStorage/SaveAsExcelFileStreamWriterHelperTests/_rels/.rels
new file mode 100644
index 00000000..9946abe7
--- /dev/null
+++ b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestData/DataStorage/SaveAsExcelFileStreamWriterHelperTests/_rels/.rels
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestData/DataStorage/SaveAsExcelFileStreamWriterHelperTests/xl/_rels/workbook.xml.rels b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestData/DataStorage/SaveAsExcelFileStreamWriterHelperTests/xl/_rels/workbook.xml.rels
new file mode 100644
index 00000000..14d5e053
--- /dev/null
+++ b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestData/DataStorage/SaveAsExcelFileStreamWriterHelperTests/xl/_rels/workbook.xml.rels
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestData/DataStorage/SaveAsExcelFileStreamWriterHelperTests/xl/styles.xml b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestData/DataStorage/SaveAsExcelFileStreamWriterHelperTests/xl/styles.xml
new file mode 100644
index 00000000..1a3bb372
--- /dev/null
+++ b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestData/DataStorage/SaveAsExcelFileStreamWriterHelperTests/xl/styles.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestData/DataStorage/SaveAsExcelFileStreamWriterHelperTests/xl/workbook.xml b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestData/DataStorage/SaveAsExcelFileStreamWriterHelperTests/xl/workbook.xml
new file mode 100644
index 00000000..ee9b4af9
--- /dev/null
+++ b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestData/DataStorage/SaveAsExcelFileStreamWriterHelperTests/xl/workbook.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestData/DataStorage/SaveAsExcelFileStreamWriterHelperTests/xl/worksheets/sheet1.xml b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestData/DataStorage/SaveAsExcelFileStreamWriterHelperTests/xl/worksheets/sheet1.xml
new file mode 100644
index 00000000..0037e7b1
--- /dev/null
+++ b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/TestData/DataStorage/SaveAsExcelFileStreamWriterHelperTests/xl/worksheets/sheet1.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+ test string
+ 3
+ 3.5
+ 0
+ 1
+
+
+ 1900-02-28
+ 61
+ 61.625
+ 0.625
+ 0.625
+ 1
+
+
+
\ No newline at end of file
diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/QueryExecution/DataStorage/SaveAsExcelFileStreamWriterHelperTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/QueryExecution/DataStorage/SaveAsExcelFileStreamWriterHelperTests.cs
new file mode 100644
index 00000000..c1e28fcc
--- /dev/null
+++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/QueryExecution/DataStorage/SaveAsExcelFileStreamWriterHelperTests.cs
@@ -0,0 +1,241 @@
+//
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+
+using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
+using Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage;
+using Microsoft.SqlTools.ServiceLayer.Test.Common;
+using Moq;
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Text.RegularExpressions;
+using System.Xml;
+using Xunit;
+
+namespace Microsoft.SqlTools.ServiceLayer.UnitTests.QueryExecution.DataStorage
+{
+ public class SaveAsExcelFileStreamWriterHelperTests : IDisposable
+ {
+ private Stream _stream;
+ public SaveAsExcelFileStreamWriterHelperTests()
+ {
+ _stream = new MemoryStream();
+ using (var helper = new SaveAsExcelFileStreamWriterHelper(_stream, true))
+ using (var sheet = helper.AddSheet())
+ {
+ DbCellValue value = new DbCellValue();
+ sheet.AddRow();
+
+ value.IsNull = true;
+ sheet.AddCell(value);
+
+ value.IsNull = false;
+ value.RawObject = "";
+ sheet.AddCell(value);
+
+ value.RawObject = "test string";
+ sheet.AddCell(value);
+
+ value.RawObject = 3;
+ sheet.AddCell(value);
+
+ value.RawObject = 3.5;
+ sheet.AddCell(value);
+
+ value.RawObject = false;
+ sheet.AddCell(value);
+
+ value.RawObject = true;
+ sheet.AddCell(value);
+
+ sheet.AddRow();
+
+ value.RawObject = new DateTime(1900, 2, 28);
+ sheet.AddCell(value);
+
+ value.RawObject = new DateTime(1900, 3, 1);
+ sheet.AddCell(value);
+
+ value.RawObject = new DateTime(1900, 3, 1, 15, 00, 00);
+ sheet.AddCell(value);
+
+ value.RawObject = new DateTime(1, 1, 1, 15, 00, 00);
+ sheet.AddCell(value);
+
+ value.RawObject = new TimeSpan(15, 00, 00);
+ sheet.AddCell(value);
+
+ value.RawObject = new TimeSpan(24, 00, 00);
+ sheet.AddCell(value);
+ }
+ }
+ Regex contentRemoveLinebreakLeadingSpace = new Regex(@"\r?\n\s*");
+ private void ContentMatch(string fileName)
+ {
+ string referencePath = Path.Combine(RunEnvironmentInfo.GetTestDataLocation(),
+ "DataStorage",
+ "SaveAsExcelFileStreamWriterHelperTests",
+ fileName);
+ string referenceContent = File.ReadAllText(referencePath);
+ referenceContent = contentRemoveLinebreakLeadingSpace.Replace(referenceContent, "");
+
+ using (ZipArchive zip = new ZipArchive(_stream, ZipArchiveMode.Read, true))
+ {
+ using (var reader = new StreamReader(zip.GetEntry(fileName).Open()))
+ {
+ string realContent = reader.ReadToEnd();
+ Assert.Equal(referenceContent, realContent);
+ }
+ }
+ }
+ [Fact]
+ public void CheckContentType()
+ {
+ ContentMatch("[Content_Types].xml");
+ }
+ [Fact]
+ public void CheckTopRels()
+ {
+ ContentMatch("_rels/.rels");
+ }
+ [Fact]
+ public void CheckWorkbookRels()
+ {
+ ContentMatch("xl/_rels/workbook.xml.rels");
+ }
+ [Fact]
+ public void CheckStyles()
+ {
+ ContentMatch("xl/styles.xml");
+ }
+ [Fact]
+ public void CheckWorkbook()
+ {
+ ContentMatch("xl/workbook.xml");
+ }
+ [Fact]
+ public void CheckSheet1()
+ {
+ ContentMatch("xl/worksheets/sheet1.xml");
+ }
+
+ public void Dispose()
+ {
+ _stream.Dispose();
+ }
+ }
+
+ public class SaveAsExcelFileStreamWriterHelperReferenceManagerTests
+ {
+ private Mock _xmlWriterMock;
+ private string LastWrittenReference { get; set; }
+ private int LastWrittenRow { get; set; }
+
+ public SaveAsExcelFileStreamWriterHelperReferenceManagerTests()
+ {
+ _xmlWriterMock = new Mock(MockBehavior.Strict);
+ _xmlWriterMock
+ .Setup(_xmlWriter => _xmlWriter.WriteChars(
+ It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((array, index, count) =>
+ LastWrittenReference = new string(array, index, count));
+ _xmlWriterMock.Setup(a => a.WriteStartAttribute(null, "r", null));
+ _xmlWriterMock.Setup(a => a.WriteEndAttribute());
+ _xmlWriterMock.Setup(a => a.WriteValue(It.IsAny()))
+ .Callback(row => LastWrittenRow = row);
+
+ }
+
+ [Fact]
+ public void ReferenceA1()
+ {
+ var xmlWriter = _xmlWriterMock.Object;
+ var manager = new SaveAsExcelFileStreamWriterHelper.ReferenceManager(xmlWriter);
+ manager.WriteAndIncreaseRowReference();
+ manager.WriteAndIncreaseColumnReference();
+ Assert.Equal("A1", LastWrittenReference);
+ }
+ [Fact]
+ public void ReferenceZ1()
+ {
+ var xmlWriter = _xmlWriterMock.Object;
+ var manager = new SaveAsExcelFileStreamWriterHelper.ReferenceManager(xmlWriter);
+ manager.WriteAndIncreaseRowReference();
+ for (int i = 0; i < 26 - 1; i++)
+ {
+ manager.IncreaseColumnReference();
+ }
+ manager.WriteAndIncreaseColumnReference();
+ Assert.Equal("Z1", LastWrittenReference);
+ manager.WriteAndIncreaseColumnReference();
+ Assert.Equal("AA1", LastWrittenReference);
+ }
+ [Fact]
+ public void ReferenceZZ1()
+ {
+ var xmlWriter = _xmlWriterMock.Object;
+ var manager = new SaveAsExcelFileStreamWriterHelper.ReferenceManager(xmlWriter);
+ manager.WriteAndIncreaseRowReference();
+
+ for (int i = 0; i < 27 * 26 - 1; i++)
+ {
+ manager.IncreaseColumnReference();
+ }
+ manager.WriteAndIncreaseColumnReference();
+ Assert.Equal("ZZ1", LastWrittenReference);
+ manager.WriteAndIncreaseColumnReference();
+ Assert.Equal("AAA1", LastWrittenReference);
+ }
+ [Fact]
+ public void ReferenceXFD()
+ {
+ var xmlWriter = _xmlWriterMock.Object;
+ var manager = new SaveAsExcelFileStreamWriterHelper.ReferenceManager(xmlWriter);
+ manager.WriteAndIncreaseRowReference();
+
+ for (int i = 0; i < 16384 - 1; i++)
+ {
+ manager.IncreaseColumnReference();
+ }
+ //The 16384 should be the maximal column and not throw
+ manager.AssureColumnReference();
+ manager.WriteAndIncreaseColumnReference();
+ Assert.Equal("XFD1", LastWrittenReference);
+ var ex = Assert.Throws(
+ () => manager.AssureColumnReference());
+ Assert.Contains("max column number is 16384", ex.Message);
+ }
+ [Fact]
+ public void ReferenceRowReset()
+ {
+ var xmlWriter = _xmlWriterMock.Object;
+ var manager = new SaveAsExcelFileStreamWriterHelper.ReferenceManager(xmlWriter);
+ manager.WriteAndIncreaseRowReference();
+ Assert.Equal(1, LastWrittenRow);
+ manager.WriteAndIncreaseColumnReference();
+ Assert.Equal("A1", LastWrittenReference);
+ manager.WriteAndIncreaseColumnReference();
+ Assert.Equal("B1", LastWrittenReference);
+
+ //add row should reset column reference
+ manager.WriteAndIncreaseRowReference();
+ Assert.Equal(2, LastWrittenRow);
+ manager.WriteAndIncreaseColumnReference();
+ Assert.Equal("A2", LastWrittenReference);
+ }
+
+ [Fact]
+ public void AddRowMustBeCalledBeforeAddCellException()
+ {
+ var xmlWriter = _xmlWriterMock.Object;
+ var manager = new SaveAsExcelFileStreamWriterHelper.ReferenceManager(xmlWriter);
+
+ var ex = Assert.Throws(
+ () => manager.AssureColumnReference());
+ Assert.Contains("AddRow must be called before AddCell", ex.Message);
+ }
+
+ }
+}