├── .gitignore ├── Excel.IO.sln ├── LICENSE ├── README.md ├── examples └── ExampleReadAndWrite │ ├── ExampleReadAndWrite.sln │ └── ExampleReadAndWrite │ ├── ExampleReadAndWrite.csproj │ ├── Person.cs │ ├── Program.cs │ └── people.xlsx ├── src └── Excel.IO │ ├── Excel.IO.csproj │ ├── ExcelColumnsAttribute.cs │ ├── ExcelConverter.cs │ ├── ExcelExtensions.cs │ ├── ExtensionMethods.cs │ ├── IExcelConverter.cs │ └── IExcelRow.cs └── test └── Excel.IO.Test ├── Excel.IO.Test.csproj ├── ExcelConverterTests.cs ├── Model └── MockExcelRow.cs └── Resources └── test.xlsx /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /Excel.IO.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.2027 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Excel.IO", "src\Excel.IO\Excel.IO.csproj", "{17EB38B4-CB15-4B34-8D35-D9861A59D742}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Excel.IO.Test", "test\Excel.IO.Test\Excel.IO.Test.csproj", "{3DB0F4D2-D803-403B-8987-70DDAEE0CE74}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {17EB38B4-CB15-4B34-8D35-D9861A59D742}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {17EB38B4-CB15-4B34-8D35-D9861A59D742}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {17EB38B4-CB15-4B34-8D35-D9861A59D742}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {17EB38B4-CB15-4B34-8D35-D9861A59D742}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {3DB0F4D2-D803-403B-8987-70DDAEE0CE74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {3DB0F4D2-D803-403B-8987-70DDAEE0CE74}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {3DB0F4D2-D803-403B-8987-70DDAEE0CE74}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {3DB0F4D2-D803-403B-8987-70DDAEE0CE74}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {89C54E98-5EFD-4682-9E5C-685E0356F50B} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | topic: sample 3 | products: 4 | - office-excel 5 | - office-365 6 | languages: 7 | - csharp 8 | extensions: 9 | contentType: tools 10 | createdDate: 6/7/2018 11:27:43 AM 11 | --- 12 | # Excel.IO 13 | 14 | The goal of this project is to simplify reading and writing Excel workbooks so that the developer needs only pass a collection of objects to write a workbook. Likewise, when reading a workbook the developer supplies a class with properties that map to column names to read a collection of those objects from the workbook. 15 | 16 | Excel.IO takes a single dependency on the [Open XML SDK](https://github.com/OfficeDev/Open-XML-SDK) and targets .NET Standard 2.0 17 | 18 | ## Features 19 | 20 | * Easy to use developer API 21 | * Write one or more worksheets per workbook by passing a collection of strongly typed objects 22 | * Read one or more worksheets from a workbook into a collection of strongly typed objects 23 | 24 | ## Limitations 25 | 26 | * Assumes workbook structure where the first row has column headers 27 | * Reading multiple worksheets is a little inefficient 28 | * Localisation isn't currently supported 29 | 30 | ## Example: Writing a worksheet 31 | 32 | Implement [IExcelRow](../master/src/Excel.IO/IExcelRow.cs) and define the columns of the spreadsheet as public properties: 33 | 34 | ```csharp 35 | public class Person : IExcelRow 36 | { 37 | public string SheetName { get => "People Sheet"; } 38 | 39 | public string EyeColour { get; set; } 40 | 41 | public int Age { get; set; } 42 | 43 | public int Height { get; set; } 44 | } 45 | ``` 46 | 47 | Then create instances and pass a collection to an instance of [ExcelConverter](../master/src/Excel.IO/ExcelConverter.cs) to write a single sheet workbook with several rows: 48 | 49 | ```csharp 50 | var people = new List(); 51 | 52 | for (int i = 0; i < 10; i++) 53 | { 54 | people.Add(new Person 55 | { 56 | EyeColour = Guid.NewGuid().ToString(), 57 | Age = new Random().Next(1, 100), 58 | Height = new Random().Next(100, 200) 59 | }); 60 | } 61 | 62 | var excelConverter = new ExcelConverter(); 63 | excelConverter.Write(people, "C:\\somefolder\\people.xlsx"); 64 | ``` 65 | 66 | ## Example: Reading a worksheet 67 | 68 | Implement [IExcelRow](../master/src/Excel.IO/IExcelRow.cs) and define public properties with the same name as columns of the spreadsheet (we'll just reuse the same class from above): 69 | 70 | ```csharp 71 | public class Person : IExcelRow 72 | { 73 | public string SheetName { get => "People Sheet"; } 74 | 75 | public string EyeColour { get; set; } 76 | 77 | public int Age { get; set; } 78 | 79 | public int Height { get; set; } 80 | } 81 | ``` 82 | 83 | Then, ask an instance of [ExcelConverter](../master/src/Excel.IO/ExcelConverter.cs) to read an IEnumerable from disk: 84 | 85 | ```csharp 86 | var excelConverter = new ExcelConverter(); 87 | var people = excelConverter.Read("C:\\somefolder\\people.xlsx"); 88 | 89 | foreach(var person in people) 90 | { 91 | //do something useful with the data 92 | } 93 | ``` 94 | 95 | ## Feedback 96 | 97 | For feature requests or bugs, please [post an issue on GitHub](https://github.com/OfficeDev/Excel-IO/issues). 98 | 99 | # Contributing 100 | 101 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 102 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 103 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 104 | 105 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 106 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 107 | provided by the bot. You will only need to do this once across all repos using our CLA. 108 | 109 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 110 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 111 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 112 | -------------------------------------------------------------------------------- /examples/ExampleReadAndWrite/ExampleReadAndWrite.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29512.175 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExampleReadAndWrite", "ExampleReadAndWrite\ExampleReadAndWrite.csproj", "{392D473C-34C3-41C7-9D5A-B29B4AF80D74}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Excel.IO", "..\..\src\Excel.IO\Excel.IO.csproj", "{7F5625C6-B879-4B46-9C1F-D0319C7A7E28}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {392D473C-34C3-41C7-9D5A-B29B4AF80D74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {392D473C-34C3-41C7-9D5A-B29B4AF80D74}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {392D473C-34C3-41C7-9D5A-B29B4AF80D74}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {392D473C-34C3-41C7-9D5A-B29B4AF80D74}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {7F5625C6-B879-4B46-9C1F-D0319C7A7E28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {7F5625C6-B879-4B46-9C1F-D0319C7A7E28}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {7F5625C6-B879-4B46-9C1F-D0319C7A7E28}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {7F5625C6-B879-4B46-9C1F-D0319C7A7E28}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {662AF04F-F976-45AE-BB8F-D3E3B8EAFA63} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /examples/ExampleReadAndWrite/ExampleReadAndWrite/ExampleReadAndWrite.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/ExampleReadAndWrite/ExampleReadAndWrite/Person.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Excel.IO.Examples 6 | { 7 | public class Person : IExcelRow 8 | { 9 | public string SheetName { get => "People Sheet"; } 10 | 11 | public string EyeColour { get; set; } 12 | 13 | public int Age { get; set; } 14 | 15 | public int Height { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/ExampleReadAndWrite/ExampleReadAndWrite/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | 5 | namespace Excel.IO.Examples 6 | { 7 | class Program 8 | { 9 | static void Main(string[] args) 10 | { 11 | var excelConverter = new ExcelConverter(); 12 | 13 | //Read Example 14 | var people = excelConverter.Read("..\\..\\..\\people.xlsx"); 15 | 16 | foreach (var person in people) 17 | { 18 | Console.WriteLine($"{person.EyeColour} : {person.Age} : {person.Height}"); 19 | } 20 | 21 | //Write Example 22 | var peopleToWrite = new List(); 23 | 24 | for (int i = 0; i < 10; i++) 25 | { 26 | peopleToWrite.Add(new Person 27 | { 28 | EyeColour = Guid.NewGuid().ToString(), 29 | Age = new Random().Next(1, 100), 30 | Height = new Random().Next(100, 200) 31 | }); 32 | } 33 | 34 | excelConverter.Write(peopleToWrite, "..\\..\\..\\newPeople.xlsx"); 35 | } 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /examples/ExampleReadAndWrite/ExampleReadAndWrite/people.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/Excel-IO/5a46ae619e88e3f6730446bf1789d6a0181f854c/examples/ExampleReadAndWrite/ExampleReadAndWrite/people.xlsx -------------------------------------------------------------------------------- /src/Excel.IO/Excel.IO.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | Library 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Excel.IO/ExcelColumnsAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | 6 | namespace Excel.IO 7 | { 8 | /// 9 | /// An attribute that allows non-property fields to be used as columns in an Excel file. This is only intended for use with IDictionary. 10 | /// 11 | [AttributeUsage(AttributeTargets.Property)] 12 | public class ExcelColumnsAttribute : Attribute 13 | { } 14 | } 15 | -------------------------------------------------------------------------------- /src/Excel.IO/ExcelConverter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using DocumentFormat.OpenXml; 5 | using DocumentFormat.OpenXml.Packaging; 6 | using DocumentFormat.OpenXml.Spreadsheet; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.IO; 10 | using System.Linq; 11 | using System.Reflection; 12 | 13 | namespace Excel.IO 14 | { 15 | /// 16 | /// Converter that allows implementations of to be exported. 17 | /// 18 | public class ExcelConverter : IExcelConverter 19 | { 20 | private SpreadsheetDocument _GetDocument(Stream stream) 21 | { 22 | var spreadsheetDocument = SpreadsheetDocument.Open(stream, isEditable: true); 23 | 24 | if (spreadsheetDocument.WorkbookPart == null) 25 | { 26 | return SpreadsheetDocument.Create(stream, SpreadsheetDocumentType.Workbook); 27 | } 28 | 29 | return spreadsheetDocument; 30 | } 31 | 32 | public void Append(IExcelRow row, Stream outputStream) 33 | { 34 | using (var spreadsheetDocument = _GetDocument(outputStream)) 35 | { 36 | this.Write([row], spreadsheetDocument); 37 | } 38 | } 39 | 40 | /// 41 | /// Exports the given rows to an Excel workbook 42 | /// 43 | /// The rows to write to the workbook. Each property will be written as a cell in the row. 44 | /// The stream to write the workbook to 45 | public void Write(IEnumerable rows, Stream outputStream) 46 | { 47 | using (var spreadsheetDocument = SpreadsheetDocument.Create(outputStream, SpreadsheetDocumentType.Workbook)) 48 | { 49 | this.Write(rows, spreadsheetDocument); 50 | } 51 | } 52 | 53 | /// 54 | /// Exports the given rows to an Excel workbook 55 | /// 56 | /// The rows to write to the workbook. Each property will be written as a cell in the row. 57 | /// The path to write the workbook to 58 | public void Write(IEnumerable rows, string path) 59 | { 60 | using (var spreadsheetDocument = SpreadsheetDocument.Create(path, SpreadsheetDocumentType.Workbook)) 61 | { 62 | this.Write(rows, spreadsheetDocument); 63 | } 64 | } 65 | 66 | private void Write(IEnumerable rows, SpreadsheetDocument spreadsheetDocument) 67 | { 68 | if (spreadsheetDocument.WorkbookPart == null) 69 | { 70 | var workbookpart = spreadsheetDocument.AddWorkbookPart(); 71 | workbookpart.Workbook = new Workbook(); 72 | } 73 | 74 | var sheets = spreadsheetDocument.WorkbookPart.Workbook.Sheets; 75 | 76 | if (sheets == null) 77 | { 78 | sheets = spreadsheetDocument.WorkbookPart.Workbook.AppendChild(new Sheets()); 79 | } 80 | 81 | var rowsGroupedBySheet = rows.GroupBy(r => r.SheetName); 82 | 83 | uint sheetId = 1; 84 | 85 | foreach (var rowGroup in rowsGroupedBySheet) 86 | { 87 | var sheetData = default(SheetData); 88 | var headerWritten = false; 89 | uint rowIndex = 1; 90 | 91 | var existingSheet = sheets.ChildElements.OfType().FirstOrDefault(s => s.Name == rowGroup.Key); 92 | 93 | if (existingSheet == null) 94 | { 95 | var worksheetPart = spreadsheetDocument.WorkbookPart.AddNewPart(); 96 | worksheetPart.Worksheet = new Worksheet(new SheetData()); 97 | 98 | var relationshipIdPart = spreadsheetDocument.WorkbookPart.GetIdOfPart(worksheetPart); 99 | var sheet = new Sheet() { Id = relationshipIdPart, SheetId = sheetId, Name = rowGroup.Key }; 100 | 101 | sheets.Append(sheet); 102 | sheetId++; 103 | 104 | sheetData = worksheetPart.Worksheet.GetFirstChild(); 105 | } 106 | else 107 | { 108 | var worksheetPart = (WorksheetPart)spreadsheetDocument.WorkbookPart.GetPartById(existingSheet.Id); 109 | sheetData = worksheetPart.Worksheet.GetFirstChild(); 110 | 111 | // get the correct row to write to 112 | var lastSheetRow = sheetData.ChildElements.OfType().Last(); 113 | rowIndex = lastSheetRow.RowIndex + 1; 114 | headerWritten = true; 115 | } 116 | 117 | // write rows to this sheet 118 | var propertiesToIgnore = typeof(IExcelRow).GetProperties(); 119 | 120 | foreach (var row in rowGroup) 121 | { 122 | var sheetRow = new Row { RowIndex = new UInt32Value(rowIndex) }; 123 | sheetData.Append(sheetRow); 124 | 125 | var properties = row.GetType().GetProperties(); 126 | var validProperties = properties.Except(propertiesToIgnore, SimpleComparer.Instance); 127 | 128 | if (!headerWritten) 129 | { 130 | this.WriteHeader(validProperties, sheetRow, row); 131 | 132 | headerWritten = true; 133 | rowIndex++; 134 | 135 | sheetRow = new Row { RowIndex = new UInt32Value(rowIndex) }; 136 | sheetData.Append(sheetRow); 137 | } 138 | 139 | this.WriteCells(validProperties, sheetRow, row); 140 | 141 | rowIndex++; 142 | } 143 | } 144 | } 145 | 146 | /// 147 | /// Reads a known workbook format into a collection of IExcelRow implementations 148 | /// 149 | /// Implementation of IExcelRow that specifies the sheet to read and the row headings to include 150 | /// Path on disk of the workbook 151 | /// A collection of 152 | public IEnumerable Read(string path) where T : IExcelRow, new() 153 | { 154 | using (var spreadsheetDocument = SpreadsheetDocument.Open(path, false)) 155 | { 156 | return this.Read(spreadsheetDocument); 157 | } 158 | } 159 | 160 | /// 161 | /// Reads a known workbook format into a collection of IExcelRow implementations 162 | /// 163 | /// Implementation of IExcelRow that specifies the sheet to read and the row headings to include 164 | /// Stream that represents the workbook 165 | /// A collection of 166 | public IEnumerable Read(Stream stream) where T : IExcelRow, new() 167 | { 168 | using (var spreadsheetDocument = SpreadsheetDocument.Open(stream, false)) 169 | { 170 | return this.Read(spreadsheetDocument); 171 | } 172 | } 173 | 174 | private IEnumerable Read(SpreadsheetDocument spreadsheetDocument) where T : IExcelRow, new() 175 | { 176 | var toReturn = new List(); 177 | var workBookPart = spreadsheetDocument.WorkbookPart; 178 | 179 | foreach (var sheet in workBookPart.Workbook.Descendants()) 180 | { 181 | var worksheetPart = workBookPart.GetPartById(sheet.Id) as WorksheetPart; 182 | 183 | if (worksheetPart == null) 184 | { 185 | // the part was supposed to be here, but wasn't found :/ 186 | continue; 187 | } 188 | 189 | if (sheet.Name.HasValue && sheet.Name.Value.Equals(new T().SheetName)) 190 | { 191 | toReturn.AddRange(this.ReadSheet(worksheetPart)); 192 | } 193 | } 194 | 195 | return toReturn; 196 | } 197 | 198 | private List ReadSheet(WorksheetPart wsPart) where T : IExcelRow, new() 199 | { 200 | var toReturn = new List(); 201 | 202 | // assume the first row contains column names 203 | var headerRow = true; 204 | var headers = new Dictionary(); 205 | 206 | foreach (var row in wsPart.Worksheet.Descendants()) 207 | { 208 | // one instance of T per row 209 | var obj = new T(); 210 | var properties = obj.GetType().GetProperties(); 211 | 212 | foreach (Cell c in row.Elements()) 213 | { 214 | var column = c.GetColumn(); 215 | var value = c.GetCellValue(); 216 | 217 | if (headerRow) 218 | { 219 | headers.Add(column, value); 220 | } 221 | else 222 | { 223 | // look for a property on the T that matches the name (ignore SheetName) 224 | object columnHeader = null; 225 | 226 | if (headers.TryGetValue(column, out columnHeader)) 227 | { 228 | var propertyInfo = properties.Where(p => 229 | p.ResolveToNameOrDisplayName().Equals(columnHeader.ToString(), StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); 230 | 231 | if (propertyInfo != null) 232 | { 233 | Type t = propertyInfo.PropertyType; 234 | t = Nullable.GetUnderlyingType(t) ?? t; 235 | 236 | if (t.IsEnum) 237 | { 238 | propertyInfo.SetValue(obj, Enum.Parse(t, (string)value)); 239 | } 240 | else 241 | { 242 | propertyInfo.SetValue(obj, Convert.ChangeType(value, t)); 243 | } 244 | } 245 | } 246 | } 247 | } 248 | 249 | if (!headerRow) 250 | { 251 | toReturn.Add(obj); 252 | } 253 | 254 | headerRow = false; 255 | } 256 | 257 | return toReturn; 258 | } 259 | 260 | private void WriteCells(IEnumerable properties, Row sheetRow, IExcelRow userRow) 261 | { 262 | var columnIndex = 0; 263 | 264 | foreach (var item in properties) 265 | { 266 | var result = _TryInsertExcelColumn(sheetRow, userRow, columnIndex, item, isHeader: false); 267 | 268 | if (result.IsExcelColumn) 269 | { 270 | columnIndex = result.ColumnIndex; 271 | continue; 272 | } 273 | 274 | var cellValue = item.GetValue(userRow); 275 | 276 | sheetRow.InsertAt( 277 | new Cell 278 | { 279 | CellReference = sheetRow.GetCellReference(columnIndex + 1), 280 | CellValue = new CellValue(cellValue == null ? string.Empty : cellValue.ToString()), 281 | DataType = new EnumValue(this.ResolveCellType(item.PropertyType)) 282 | }, 283 | columnIndex); 284 | 285 | columnIndex++; 286 | } 287 | } 288 | 289 | private CellValues ResolveCellType(Type propertyType) 290 | { 291 | var nullableType = Nullable.GetUnderlyingType(propertyType); 292 | 293 | if (nullableType != null) 294 | { 295 | propertyType = Nullable.GetUnderlyingType(propertyType); 296 | } 297 | 298 | // TODO: Support date? 299 | switch (Type.GetTypeCode(propertyType)) 300 | { 301 | case TypeCode.Decimal: 302 | case TypeCode.Double: 303 | case TypeCode.Int16: 304 | case TypeCode.Int32: 305 | case TypeCode.Int64: 306 | case TypeCode.UInt16: 307 | case TypeCode.UInt32: 308 | case TypeCode.UInt64: 309 | { 310 | return CellValues.Number; 311 | } 312 | default: 313 | { 314 | return CellValues.String; 315 | } 316 | } 317 | } 318 | 319 | private void WriteHeader(IEnumerable properties, Row sheetRow, IExcelRow userRow) 320 | { 321 | var columnIndex = 0; 322 | 323 | foreach (var item in properties) 324 | { 325 | var result = _TryInsertExcelColumn(sheetRow, userRow, columnIndex, item, isHeader: true); 326 | 327 | if (result.IsExcelColumn) 328 | { 329 | columnIndex = result.ColumnIndex; 330 | continue; 331 | } 332 | 333 | var headerName = item.Name; 334 | 335 | var displayNameAttr = item.GetCustomAttribute(true); 336 | 337 | if (displayNameAttr != null) 338 | { 339 | headerName = displayNameAttr.DisplayName; 340 | } 341 | 342 | sheetRow.InsertAt( 343 | new Cell 344 | { 345 | CellReference = sheetRow.GetCellReference(columnIndex + 1), 346 | CellValue = new CellValue(headerName), 347 | DataType = new EnumValue(CellValues.String) 348 | }, 349 | columnIndex); 350 | 351 | columnIndex++; 352 | } 353 | } 354 | 355 | private InsertExcelColumnResult _TryInsertExcelColumn(Row sheetRow, IExcelRow row, int columnIndex, PropertyInfo item, bool isHeader) 356 | { 357 | var excelColumnsAttr = item.GetCustomAttribute(true); 358 | 359 | if (excelColumnsAttr != null) 360 | { 361 | var dict = (IDictionary)item.GetValue(row); 362 | 363 | if (dict != null) 364 | { 365 | foreach (var kvp in dict) 366 | { 367 | sheetRow.InsertAt( 368 | new Cell 369 | { 370 | CellReference = sheetRow.GetCellReference(columnIndex + 1), 371 | CellValue = new CellValue(isHeader ? 372 | kvp.Key : 373 | kvp.Value == null ? 374 | string.Empty : kvp.Value), 375 | DataType = new EnumValue(isHeader ? 376 | CellValues.String : 377 | this.ResolveCellType(item.PropertyType)) 378 | }, 379 | columnIndex); 380 | 381 | columnIndex++; 382 | } 383 | 384 | return new InsertExcelColumnResult { IsExcelColumn = true, ColumnIndex = columnIndex }; 385 | } 386 | } 387 | 388 | return InsertExcelColumnResult.IsNotExcelColumn; 389 | } 390 | 391 | private class InsertExcelColumnResult 392 | { 393 | private static readonly InsertExcelColumnResult _IsNotExcelColumn = new InsertExcelColumnResult { IsExcelColumn = false }; 394 | 395 | public static InsertExcelColumnResult IsNotExcelColumn 396 | { 397 | get { return _IsNotExcelColumn; } 398 | } 399 | 400 | public int ColumnIndex { get; set; } 401 | 402 | public bool IsExcelColumn { get; set; } 403 | } 404 | 405 | private class SimpleComparer : IEqualityComparer 406 | { 407 | private static readonly SimpleComparer ReadonlyInstance; 408 | 409 | static SimpleComparer() 410 | { 411 | ReadonlyInstance = new SimpleComparer(); 412 | } 413 | 414 | public static SimpleComparer Instance 415 | { 416 | get { return ReadonlyInstance; } 417 | } 418 | 419 | public bool Equals(PropertyInfo x, PropertyInfo y) 420 | { 421 | return x.Name == y.Name; 422 | } 423 | 424 | public int GetHashCode(PropertyInfo obj) 425 | { 426 | // only care if the name of the property info matches 427 | return obj.Name.GetHashCode(); 428 | } 429 | } 430 | } 431 | } 432 | -------------------------------------------------------------------------------- /src/Excel.IO/ExcelExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using DocumentFormat.OpenXml.Packaging; 5 | using DocumentFormat.OpenXml.Spreadsheet; 6 | using System; 7 | using System.Linq; 8 | using System.Reflection; 9 | 10 | namespace Excel.IO 11 | { 12 | public static class ExcelExtensions 13 | { 14 | private static readonly char FIRST_LETTER = 'A'; 15 | 16 | public static string GetCellReference(this Row row, int columnIndex) 17 | { 18 | var cellReference = string.Empty; 19 | 20 | while (columnIndex > 0) 21 | { 22 | var remainder = (columnIndex - 1) % 26; 23 | var letter = (char)(FIRST_LETTER + remainder); 24 | cellReference = letter + cellReference; 25 | columnIndex = (columnIndex - 1) / 26; 26 | } 27 | 28 | return $"{cellReference}{row.RowIndex}"; 29 | } 30 | 31 | /// 32 | /// Finds the column identifier for a given cell, ie: A 33 | /// 34 | /// The Cell to find the column for 35 | /// The column name 36 | public static string GetColumn(this Cell cell) 37 | { 38 | if (!cell.CellReference.HasValue) 39 | { 40 | return string.Empty; 41 | } 42 | 43 | var endIndex = 0; 44 | 45 | for (int i = 0; i < cell.CellReference.Value.Length; i++) 46 | { 47 | if (char.IsLetter(cell.CellReference.Value[i])) 48 | { 49 | endIndex = i + 1; 50 | } 51 | } 52 | 53 | return cell.CellReference.Value.Substring(0, endIndex); 54 | } 55 | 56 | /// 57 | /// Returns the value for a given Cell, taking into account the shared string table 58 | /// 59 | /// The Cell to get the value for 60 | /// The value of the Cell or null 61 | public static object GetCellValue(this Cell cell) 62 | { 63 | if (cell == null) 64 | { 65 | return null; 66 | } 67 | 68 | var worksheet = cell.FindParentWorksheet(); 69 | 70 | if (string.IsNullOrWhiteSpace(cell.DataType)) 71 | { 72 | if (cell.StyleIndex == null || 73 | !cell.StyleIndex.HasValue) 74 | { 75 | // General 76 | 77 | if (cell.CellFormula != null) 78 | { 79 | return cell.CellValue.Text.ReplaceDecimalSeparator(); 80 | } 81 | else 82 | { 83 | return cell.InnerText.ReplaceDecimalSeparator(); 84 | } 85 | } 86 | 87 | var document = worksheet.WorksheetPart.OpenXmlPackage as SpreadsheetDocument; 88 | var styleSheet = document.WorkbookPart.WorkbookStylesPart.Stylesheet; 89 | var cellStyle = styleSheet.CellFormats.ChildElements[(int)cell.StyleIndex.Value]; 90 | var formatId = (cellStyle as CellFormat).NumberFormatId; 91 | 92 | switch (((int)formatId.Value)) 93 | { 94 | // Linked Cell 95 | case 0: 96 | return cell.CellValue == null ? string.Empty : cell.CellValue.Text; 97 | 98 | // Numbers 99 | // TODO: Find out if only integers fall into this case, or if all numeric data types do as well 100 | case 1: 101 | if (cell.CellFormula != null) 102 | { 103 | return cell.CellValue.Text.ReplaceDecimalSeparator(); 104 | } 105 | else 106 | { 107 | return cell.InnerText.ReplaceDecimalSeparator(); 108 | } 109 | 110 | // Percentage 111 | case 9: 112 | 113 | // Scientific Notation 114 | case 11: 115 | 116 | // Fraction 117 | case 10: 118 | case 12: 119 | if (cell.CellFormula != null) 120 | { 121 | return float.Parse(cell.CellValue.Text.ReplaceDecimalSeparator()); 122 | } 123 | else 124 | { 125 | return float.Parse(cell.InnerText.ReplaceDecimalSeparator()); 126 | } 127 | 128 | // General 129 | case 44: 130 | if (cell.CellFormula != null) 131 | { 132 | return cell.CellValue.Text.ReplaceDecimalSeparator(); 133 | } 134 | else 135 | { 136 | return cell.InnerText.ReplaceDecimalSeparator(); 137 | } 138 | 139 | // Text 140 | case 49: 141 | if (cell.CellFormula != null) 142 | { 143 | return cell.CellValue.Text; 144 | } 145 | else 146 | { 147 | return cell.InnerText; 148 | } 149 | 150 | // Date 151 | case 14: 152 | case 15: 153 | case 16: 154 | case 17: 155 | case 18: 156 | case 19: 157 | case 20: 158 | case 21: 159 | case 22: 160 | case 164: 161 | case 165: 162 | case 166: 163 | case 169: 164 | cell.TryParseDate(out var date); 165 | return date; 166 | 167 | // Phone Number 168 | // TODO: Format Phone Numbers 169 | case 168: 170 | return cell.CellValue.Text; 171 | 172 | // Currency 173 | case 167: 174 | if (cell.CellFormula != null) 175 | { 176 | return decimal.Parse(cell.CellValue.Text); 177 | } 178 | else 179 | { 180 | return decimal.Parse(cell.InnerText); 181 | } 182 | default: 183 | throw new NotImplementedException($"Format with ID {(int)formatId.Value} and value {cell.CellValue?.InnerText ?? cell.InnerText} wasn't handled and needs to be parsed to the right format!"); 184 | } 185 | } 186 | 187 | if (cell.DataType.Value == CellValues.SharedString) 188 | { 189 | var sharedStringTablePart = worksheet.FindSharedStringTablePart(); 190 | 191 | if (sharedStringTablePart != null && 192 | sharedStringTablePart.SharedStringTable != null) 193 | { 194 | return sharedStringTablePart.SharedStringTable.ElementAt(int.Parse(cell.InnerText)).InnerText; 195 | } 196 | } 197 | else if (cell.DataType.Value == CellValues.Boolean) 198 | { 199 | return cell.InnerText == "0" ? false : true; 200 | } 201 | 202 | if (cell.CellFormula != null) 203 | { 204 | return cell.CellValue.Text; 205 | } 206 | else 207 | { 208 | return cell.InnerText; 209 | } 210 | } 211 | 212 | public static bool TryParseDate(this Cell cell, out DateTime? date) 213 | { 214 | date = null; 215 | 216 | if (cell.StyleIndex == null || 217 | !cell.StyleIndex.HasValue) 218 | { 219 | return false; 220 | } 221 | 222 | // See SpreadsheetML Reference at 18.8.30 numFmt(Number Format) for more detail: www.ecma-international.org/publications/standards/Ecma-376.htm 223 | // Also note some Excel specific variations 224 | // The standard defines built-in format ID 14: "mm-dd-yy"; 22: "m/d/yy h:mm"; 37: "#,##0 ;(#,##0)"; 38: "#,##0 ;[Red](#,##0)"; 39: "#,##0.00;(#,##0.00)"; 40: "#,##0.00;[Red](#,##0.00)"; 47: "mmss.0"; KOR fmt 55: "yyyy-mm-dd". 225 | // Excel defines built-in format ID 14: "m/d/yyyy"; 22: "m/d/yyyy h:mm"; 37: "#,##0_);(#,##0)"; 38: "#,##0_);[Red](#,##0)"; 39: "#,##0.00_);(#,##0.00)"; 40: "#,##0.00_);[Red](#,##0.00)"; 47: "mm:ss.0"; KOR fmt 55: "yyyy/mm/dd". 226 | 227 | if (cell.CellFormula != null) 228 | { 229 | date = DateTime.FromOADate(double.Parse(cell.CellValue.Text.ReplaceDecimalSeparator())); 230 | } 231 | else if (string.IsNullOrWhiteSpace(cell.InnerText)) 232 | { 233 | date = DateTime.FromOADate(2); 234 | return true; 235 | } 236 | else 237 | { 238 | date = DateTime.FromOADate(double.Parse(cell.InnerText.ReplaceDecimalSeparator())); 239 | } 240 | return true; 241 | } 242 | 243 | public static Worksheet FindParentWorksheet(this Cell cell) 244 | { 245 | var parent = cell.Parent; 246 | 247 | while (parent.Parent != null && 248 | parent.Parent != parent && 249 | !parent.LocalName.Equals("worksheet", StringComparison.InvariantCultureIgnoreCase)) 250 | { 251 | parent = parent.Parent; 252 | } 253 | 254 | if (!parent.LocalName.Equals("worksheet", StringComparison.InvariantCultureIgnoreCase)) 255 | { 256 | throw new Exception("Worksheet invalid"); 257 | } 258 | 259 | return parent as Worksheet; 260 | } 261 | 262 | public static SharedStringTablePart FindSharedStringTablePart(this Worksheet worksheet) 263 | { 264 | var document = worksheet.WorksheetPart.OpenXmlPackage as SpreadsheetDocument; 265 | 266 | return document.WorkbookPart.GetPartsOfType().FirstOrDefault(); 267 | } 268 | 269 | public static string ResolveToNameOrDisplayName(this PropertyInfo item) 270 | { 271 | var displayNameAttr = item.GetCustomAttributes(typeof(System.ComponentModel.DisplayNameAttribute), true).Cast().FirstOrDefault(); 272 | 273 | if (displayNameAttr != null) 274 | { 275 | return displayNameAttr.DisplayName; 276 | } 277 | 278 | return item.Name; 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/Excel.IO/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Globalization; 7 | using System.Text; 8 | 9 | namespace Excel.IO 10 | { 11 | public static class ExtensionMethods 12 | { 13 | public static string ReplaceDecimalSeparator(this string text) 14 | { 15 | return text.Replace('.', Convert.ToChar(CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator)).Replace(',', Convert.ToChar(CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator)); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Excel.IO/IExcelConverter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System.Collections.Generic; 5 | using System.IO; 6 | 7 | namespace Excel.IO 8 | { 9 | public interface IExcelConverter 10 | { 11 | void Write(IEnumerable rows, Stream outputStream); 12 | 13 | void Write(IEnumerable rows, string path); 14 | 15 | IEnumerable Read(string path) where T : IExcelRow, new(); 16 | 17 | IEnumerable Read(Stream stream) where T : IExcelRow, new(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Excel.IO/IExcelRow.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | namespace Excel.IO 5 | { 6 | public interface IExcelRow 7 | { 8 | string SheetName { get; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/Excel.IO.Test/Excel.IO.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /test/Excel.IO.Test/ExcelConverterTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using DocumentFormat.OpenXml.Spreadsheet; 5 | using Excel.IO.Test.Model; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Reflection; 11 | using Xunit; 12 | 13 | namespace Excel.IO.Test 14 | { 15 | public class ExcelConverterTests : IDisposable 16 | { 17 | private Stream xlsxTestResource; 18 | 19 | public ExcelConverterTests() 20 | { 21 | var res = 22 | Assembly.GetExecutingAssembly().GetManifestResourceStream("Excel.IO.Test.Resources.test.xlsx"); 23 | 24 | StreamReader sr = new StreamReader(res); 25 | this.xlsxTestResource = sr.BaseStream; 26 | } 27 | 28 | public void Dispose() 29 | { 30 | this.xlsxTestResource.Close(); 31 | this.xlsxTestResource.Dispose(); 32 | } 33 | 34 | [Fact] 35 | public void ExcelConverter_Can_Write_A_Single_Sheet_Workbook() 36 | { 37 | var excelConverter = new ExcelConverter(); 38 | 39 | var sheetName = "Sheet1"; 40 | var rows = new List(); 41 | 42 | for (int i = 0; i < 100; i++) 43 | { 44 | var mockRow = new MockExcelRow(); 45 | mockRow.SheetName = sheetName; 46 | 47 | rows.Add(mockRow); 48 | } 49 | 50 | using (var result = new MemoryStream()) 51 | { 52 | excelConverter.Write(rows, result); 53 | Assert.True(result.Length > 0); 54 | } 55 | } 56 | 57 | [Fact] 58 | public void ExcelConverter_Can_Write_A_MultiSheet_Workbook() 59 | { 60 | var excelConverter = new ExcelConverter(); 61 | var rows = new List(); 62 | 63 | for (int i = 0; i < 100; i++) 64 | { 65 | var mockRow = new MockExcelRow(); 66 | mockRow.SheetName = $"Sheet{i}"; 67 | 68 | rows.Add(mockRow); 69 | } 70 | 71 | using (var result = new MemoryStream()) 72 | { 73 | excelConverter.Write(rows, result); 74 | Assert.True(result.Length > 0); 75 | } 76 | } 77 | 78 | [Fact] 79 | public void ExcelConverter_Can_Read_A_Single_Sheet_From_A_Workbook() 80 | { 81 | var excelConverter = new ExcelConverter(); 82 | var result = excelConverter.Read(this.xlsxTestResource); 83 | 84 | Assert.Equal(10, result.Count()); 85 | } 86 | 87 | [Fact] 88 | public void ExcelConverter_Can_Read_Multiple_Sheets_From_A_Workbook() 89 | { 90 | var excelConverter = new ExcelConverter(); 91 | var result1 = excelConverter.Read(this.xlsxTestResource); 92 | var result2 = excelConverter.Read(this.xlsxTestResource); 93 | var result3 = excelConverter.Read(this.xlsxTestResource); 94 | 95 | Assert.Equal(10, result1.Count()); 96 | Assert.Equal(4, result2.Count()); 97 | Assert.Equal(10, result3.Count()); 98 | } 99 | 100 | [Fact] 101 | public void ExcelConverter_Can_Read_A_Single_Row_From_A_Sheet() 102 | { 103 | var excelConverter = new ExcelConverter(); 104 | List rows = (List)excelConverter.Read(xlsxTestResource); 105 | 106 | Assert.NotEmpty(rows); 107 | rows[0].GetType().GetProperties().ToList().ForEach(property => 108 | { 109 | Assert.NotNull(property.GetValue(rows[0])); 110 | }); 111 | } 112 | 113 | [Fact] 114 | public void ExcelConverter_Can_Read_Multiple_Rows_From_Multiple_Sheet() 115 | { 116 | var excelConverter = new ExcelConverter(); 117 | List rows = (List)excelConverter.Read(xlsxTestResource); 118 | 119 | Assert.NotEmpty(rows); 120 | 121 | rows.ForEach(row => 122 | { 123 | row.GetType().GetProperties().ToList().ForEach(property => 124 | { 125 | Assert.NotNull(property.GetValue(row)); 126 | }); 127 | }); 128 | } 129 | 130 | [Fact] 131 | public void ExcelConverter_Can_Read_Multiple_Rows_From_A_Sheet() 132 | { 133 | var excelConverter = new ExcelConverter(); 134 | List> sheets = new List>(); 135 | sheets.Add(excelConverter.Read(this.xlsxTestResource).ToList()); 136 | sheets.Add(excelConverter.Read(this.xlsxTestResource).ToList()); 137 | sheets.Add(excelConverter.Read(this.xlsxTestResource).ToList()); 138 | 139 | sheets.ForEach(sheet => 140 | { 141 | Assert.NotEmpty(sheet); 142 | 143 | sheet.ForEach(row => 144 | { 145 | row.GetType().GetProperties().ToList().ForEach(property => 146 | { 147 | Assert.NotNull(property.GetValue(row)); 148 | }); 149 | }); 150 | }); 151 | } 152 | 153 | [Fact] 154 | public void Cell_References_Correctly_Increment_Column_Letters() 155 | { 156 | var row = new Row(); 157 | row.RowIndex = 1; 158 | 159 | var expectedCells = new[] { "A1", "B1", "C1", "D1" }; 160 | 161 | var actualCells = new List(); 162 | 163 | for (int i = 1; i < 5; i++) 164 | { 165 | var cellRef = row.GetCellReference(i); 166 | actualCells.Add(cellRef); 167 | } 168 | 169 | foreach (var expectedCell in expectedCells) 170 | { 171 | Assert.Equal(expectedCell, actualCells[Array.IndexOf(expectedCells, expectedCell)]); 172 | } 173 | } 174 | 175 | [Fact] 176 | public void Columns_27_And_28_Are_Handled_Correctly() 177 | { 178 | var row = new Row(); 179 | row.RowIndex = 1; 180 | 181 | var cellRef = row.GetCellReference(27); 182 | Assert.Equal("AA1", cellRef); 183 | 184 | var cellRef2 = row.GetCellReference(28); 185 | Assert.Equal("AB1", cellRef2); 186 | } 187 | 188 | [Fact] 189 | public void Columns_53_And_54_Are_Handled_Correctly() 190 | { 191 | var row = new Row(); 192 | row.RowIndex = 1; 193 | 194 | var cellRef = row.GetCellReference(53); 195 | Assert.Equal("BA1", cellRef); 196 | 197 | var cellRef2 = row.GetCellReference(54); 198 | Assert.Equal("BB1", cellRef2); 199 | } 200 | 201 | [Fact] 202 | public void Cell_References_Correct_Row_Number() 203 | { 204 | var row = new Row(); 205 | row.RowIndex = 4; 206 | 207 | var cellRef = row.GetCellReference(1); 208 | 209 | Assert.Equal("A4", cellRef); 210 | } 211 | 212 | [Fact] 213 | public void Sheets_Written_Can_Be_Read() 214 | { 215 | var excelConverter = new ExcelConverter(); 216 | var written = new[] 217 | { 218 | new MockExcelRow3 219 | { 220 | Address = "123 Fake", 221 | FirstName = "John", 222 | LastName = "Doe", 223 | LastContact = DateTime.Now, 224 | CustomerId = 1, 225 | IsActive = true, 226 | Balance = 100.00m, 227 | Category = Category.CategoryA 228 | } 229 | }; 230 | 231 | var tmpFile = Path.GetTempFileName(); 232 | 233 | try 234 | { 235 | excelConverter.Write(written, tmpFile); 236 | 237 | var read = excelConverter.Read(tmpFile); 238 | 239 | Assert.Equal(written.Length, read.Count()); 240 | Assert.Equal(written.First().Address, read.First().Address); 241 | } 242 | finally 243 | { 244 | System.IO.File.Delete(tmpFile); 245 | } 246 | } 247 | 248 | [Fact] 249 | public void ExcelColumnsAttribute_Correctly_WriteColumns_For_Dictionary_Keys_And_Row_Values_For_Dictionary_Value() 250 | { 251 | var excelConverter = new ExcelConverter(); 252 | var written = new[] 253 | { 254 | new MockExcelRow6 255 | { 256 | CustomProperties = new Dictionary 257 | { 258 | { "Key1", "Value1" }, 259 | { "Key2", "Value2" }, 260 | { "Key3", "Value3" } 261 | } 262 | } 263 | }; 264 | 265 | var tmpFile = Path.GetTempFileName(); 266 | 267 | try 268 | { 269 | excelConverter.Write(written, tmpFile); 270 | 271 | var read = excelConverter.Read(tmpFile); 272 | 273 | Assert.Equal(written.Length, read.Count()); 274 | 275 | //it's implied that the header is being written correctly as the Key1, Key2, Key3 can only be read if the header is written correctly 276 | Assert.Equal(written.First().CustomProperties.First().Value, read.First().Key1); 277 | Assert.Equal(written.First().CustomProperties.Skip(1).First().Value, read.First().Key2); 278 | Assert.Equal(written.First().CustomProperties.Skip(2).First().Value, read.First().Key3); 279 | } 280 | finally 281 | { 282 | System.IO.File.Delete(tmpFile); 283 | } 284 | } 285 | 286 | [Fact] 287 | public void Appending_Is_Successful_When_The_File_To_Append_To_Does_Not_Already_Exist() 288 | { 289 | var excelConverter = new ExcelConverter(); 290 | var expected = new MockExcelRow6ExplicitProperties { Key1 = "a", Key2 = "b", Key3 = "c" }; 291 | 292 | var tmpFile = Path.GetTempFileName(); 293 | 294 | using (var fileStreamWrite = new FileStream(tmpFile, FileMode.Create)) 295 | { 296 | excelConverter.Append(expected, fileStreamWrite); 297 | } 298 | 299 | using (var fileStreamRead = new FileStream(tmpFile, FileMode.Open)) 300 | { 301 | var actual = excelConverter.Read(fileStreamRead); 302 | 303 | Assert.Equal(expected.Key1, actual.First().Key1); 304 | Assert.Equal(expected.Key2, actual.First().Key2); 305 | Assert.Equal(expected.Key3, actual.First().Key3); 306 | } 307 | } 308 | 309 | [Fact] 310 | public void Appending_Is_Successful_When_The_File_To_Append_To_Does_Already_Exist() 311 | { 312 | var excelConverter = new ExcelConverter(); 313 | var expectedRow1 = new MockExcelRow6ExplicitProperties { Key1 = "a", Key2 = "b", Key3 = "c" }; 314 | var expectedRow2 = new MockExcelRow6ExplicitProperties { Key1 = "d", Key2 = "e", Key3 = "f" }; 315 | 316 | var tmpFile = Path.GetTempFileName(); 317 | 318 | using (var fileStreamWrite1 = new FileStream(tmpFile, FileMode.Create)) 319 | { 320 | excelConverter.Append(expectedRow1, fileStreamWrite1); 321 | } 322 | 323 | using (var fileStreamWrite2 = new FileStream(tmpFile, FileMode.Open)) 324 | { 325 | excelConverter.Append(expectedRow2, fileStreamWrite2); 326 | } 327 | 328 | using (var fileStreamRead = new FileStream(tmpFile, FileMode.Open)) 329 | { 330 | var rows = excelConverter.Read(fileStreamRead); 331 | var actualRow1 = rows.First(); 332 | var actualRow2 = rows.Skip(1).First(); 333 | 334 | Assert.Equal(expectedRow1.Key1, actualRow1.Key1); 335 | Assert.Equal(expectedRow1.Key2, actualRow1.Key2); 336 | Assert.Equal(expectedRow1.Key3, actualRow1.Key3); 337 | 338 | Assert.Equal(expectedRow2.Key1, actualRow2.Key1); 339 | Assert.Equal(expectedRow2.Key2, actualRow2.Key2); 340 | Assert.Equal(expectedRow2.Key3, actualRow2.Key3); 341 | } 342 | } 343 | } 344 | } -------------------------------------------------------------------------------- /test/Excel.IO.Test/Model/MockExcelRow.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.ComponentModel; 7 | 8 | namespace Excel.IO.Test.Model 9 | { 10 | public class MockExcelRow : IExcelRow 11 | { 12 | public string SheetName { get; set; } 13 | 14 | public DateTime LastContact { get; set; } 15 | 16 | public int CustomerId { get; set; } 17 | 18 | public string FirstName { get; set; } 19 | 20 | public string LastName { get; set; } 21 | 22 | public string Address { get; set; } 23 | 24 | public bool IsActive { get; set; } 25 | 26 | public decimal Balance { get; set; } 27 | 28 | public Category Category { get; set; } 29 | } 30 | 31 | public class MockExcelRow2 : IExcelRow 32 | { 33 | public string SheetName { get; set; } 34 | 35 | [DisplayName("Eye Colour")] 36 | public string EyeColour { get; set; } 37 | 38 | public int Age { get; set; } 39 | 40 | public int Height { get; set; } 41 | } 42 | 43 | public class MockExcelRow3 : MockExcelRow 44 | { 45 | public MockExcelRow3() 46 | { 47 | this.SheetName = "Sheet1"; 48 | } 49 | } 50 | 51 | public class MockExcelRow4 : MockExcelRow2 52 | { 53 | public MockExcelRow4() 54 | { 55 | this.SheetName = "Sheet2"; 56 | } 57 | } 58 | 59 | public class MockExcelRow5 : IExcelRow 60 | { 61 | public string SheetName { get; set; } 62 | public DateTime LastContact { get; set; } 63 | 64 | public int CustomerId { get; set; } 65 | 66 | public string FirstName { get; set; } 67 | 68 | public string LastName { get; set; } 69 | 70 | public string Address { get; set; } 71 | 72 | public bool IsActive { get; set; } 73 | 74 | public decimal Balance { get; set; } 75 | 76 | public Category Category { get; set; } 77 | 78 | public int Age { get; set; } 79 | 80 | public bool IsMarried { get; set; } 81 | 82 | public string PhoneNumber { get; set; } 83 | 84 | public string Email { get; set; } 85 | 86 | public decimal Debt { get; set; } 87 | 88 | public decimal HouseholdIncome { get; set; } 89 | 90 | public float AgePercentage { get; set; } 91 | 92 | public DateTime BirthDate { get; set; } 93 | 94 | public float ProbabilityOfSameAge { get; set; } 95 | 96 | public float Constants { get; set; } 97 | 98 | public DateTime LongDate { get; set; } 99 | 100 | public DateTime LongDate2 { get; set; } 101 | 102 | public DateTime DayMonth { get; set; } 103 | 104 | public DateTime Something { get; set; } 105 | 106 | public Category Category1 { get; set; } 107 | 108 | public Category Category2 { get; set; } 109 | 110 | public Category Category3 { get; set; } 111 | 112 | public Category Category4 { get; set; } 113 | 114 | public Category Category5 { get; set; } 115 | 116 | public Category Category6 { get; set; } 117 | 118 | public Category Category7 { get; set; } 119 | public MockExcelRow5() { SheetName = "Sheet3"; } 120 | } 121 | 122 | public class MockExcelRow6 : IExcelRow 123 | { 124 | public string SheetName => "Sheet1"; 125 | 126 | [ExcelColumns] 127 | public Dictionary CustomProperties { get; set; } = new Dictionary(); 128 | } 129 | 130 | public class MockExcelRow6ExplicitProperties : IExcelRow 131 | { 132 | public string SheetName => "Sheet1"; 133 | 134 | public string Key1 { get; set; } 135 | 136 | public string Key2 { get; set; } 137 | 138 | public string Key3 { get; set; } 139 | } 140 | 141 | 142 | public enum Category 143 | { 144 | CategoryA, 145 | CategoryB, 146 | CategoryC 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /test/Excel.IO.Test/Resources/test.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OfficeDev/Excel-IO/5a46ae619e88e3f6730446bf1789d6a0181f854c/test/Excel.IO.Test/Resources/test.xlsx --------------------------------------------------------------------------------