├── .gitattributes ├── .gitignore ├── LICENSE ├── changelog.md ├── readme.md └── src ├── Benchmarks ├── Benchmarks.csproj ├── Program.cs ├── Properties │ ├── PublishProfiles │ │ └── FolderProfile.pubxml │ └── launchSettings.json └── Tests │ ├── BenchmarkRead.cs │ └── BenchmarkWrite.cs ├── OpenSpreadsheet.sln ├── OpenSpreadsheet ├── BidirectionalDictionary.cs ├── Configuration │ ├── ClassMap.cs │ ├── ColumnStyle.cs │ ├── ConfigurationValidator.cs │ ├── PropertyMap.cs │ ├── PropertyMapData.cs │ ├── StylesCollection.cs │ └── WorksheetStyle.cs ├── Enums │ ├── BorderPlacement.cs │ ├── ColumnType.cs │ └── OpenXmlNumberingFormat.cs ├── Extensions │ └── TypeExtensions.cs ├── OpenSpreadsheet.csproj ├── ReaderRow.cs ├── Spreadsheet.cs ├── WorksheetReader.cs └── WorksheetWriter.cs └── Tests ├── Configuration ├── ClassMaps.cs └── ImpliedMappings.cs ├── DataConversions ├── Constants.cs ├── ConvertUsing.cs ├── Defaults.cs └── NumberFormats.cs ├── SpreadsheetIO ├── Reader.cs └── Writer.cs ├── SpreadsheetTesterBase.cs ├── SpreadsheetValidator.cs ├── Styles ├── Alignments.cs ├── BorderStyles.cs ├── CellPattern.cs ├── Fonts.cs └── WorksheetStyles.cs └── Tests.csproj /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc 262 | /src/ConsoleApp1 263 | /src/WikiExamples -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 FolkCoder 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 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Release Log 2 | 3 | ## v1.2.3 4 | 5 | ### Bug Fixes 6 | + Removed `ActiveCell` from the selection pane when writing a frozen header row; it does not seem necessary and creates a corrupt worksheet unless the entire row sequence is specified. 7 | 8 | ## v1.2.2 9 | 10 | ### Bug Fixes 11 | + Fixed a bug in which `WorksheetReader` and `WorksheetWriter` were still not converting between a `double` and mapped numeric types. 12 | 13 | ## v1.2.1 14 | 15 | ### Bug Fixes 16 | + Fixed `ShouldFreezeHeaderRow` when using a non-default header, and added a better test case to capture the scenario. 17 | 18 | ## v1.2.0 19 | 20 | ### Enhancements 21 | + Continued refactoring test cases to automate testing of additional scenarios. 22 | + Added automated tests to validate and read worksheets generated by OpenSpreadsheet and subsequently saved by Excel, as Excel automatically modifies XML and causes unexpected errors. 23 | + Renamed `ShouldFreezeTopRow` worksheet style property to `ShouldFreezeHeaderRow`. 24 | + `ShouldAutoFilter` now acts on the user-specified header row, rather than the first row. 25 | + Header styles are now cached regardless of the value of `ShouldWriteHeaderRow`, which will allow for greater precision when using an explicit `WorksheetWriter`. 26 | 27 | ### Bug Fixes 28 | + Changed `WorksheetReader` to handle numbers stored in scientific notation. 29 | 30 | ## v1.1.3 31 | 32 | ### Enhancements 33 | + Began refactoring test cases to automate common scenarios rather than relying on file types and manual scanning. 34 | 35 | ### Bug Fixes 36 | + Null cell data types now default to numeric during reading (Excel apparently removes this attribute). 37 | + Trying to pass Color.Transparent value to various spreadsheet style items will now throw an `ArgumentException`. 38 | 39 | ## v1.1.2 40 | 41 | ### Enhancements 42 | + Added `Accounting` numbering format. 43 | 44 | ### Bug Fixes 45 | + `WorksheetReader` will no longer try to set properties without a public setter. 46 | + Fixed bug that skipped constants when reading. 47 | 48 | 49 | 50 | ## v1.1.1 51 | 52 | ### Bug Fixes 53 | + Fixed a bug that duplicated style definitions when reading. 54 | 55 | ## v1.1.0 56 | 57 | ### Enhancements 58 | + Added `ReadUsing` and `WriteUsing` methods (and associated tests) to allow for custom logic to be applied to read and write maps. 59 | + Added `ReaderRow` class to handle read operations (used for `ReadUsing`). 60 | + Created more accurate tests for `Default` configuration methods. 61 | 62 | 63 | 64 | ## v1.0.0 65 | 66 | Initial release. -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # OpenSpreadsheet 2 | [![Nuget Badge](https://img.shields.io/nuget/v/OpenSpreadsheet.svg?style=for-the-badge)](https://www.nuget.org/packages/OpenSpreadsheet/) 3 | 4 | OpenSpreadsheet is a fast and lightweight wrapper around the OpenXml spreadsheet library, employing an easy-to-use fluent interface to define relations between entities and spreadsheet rows. The library uses the Simple API for XML (SAX) method for both reading and writing. 5 | 6 | The primary use case for OpenSpreadsheet is efficiently importing and exporting typed collections, where each row roughly corresponds to a class instance. It is not meant to offer fine-grained control of data or formatting at the cell level; if you need this level of control, check out [ClosedXml](https://github.com/ClosedXML/ClosedXML) or [EPPlus](https://github.com/JanKallman/EPPlus). 7 | 8 | 9 | ## Syntax 10 | 11 | ### Configuration 12 | 13 | OpenSpreadsheet uses a fluent interface to map object properties to spreadsheet rows. The configuration format is modeled after the fantastic [CsvHelper](https://joshclose.github.io/CsvHelper/) library, although OpenSpreadsheet has far fewer mapping options (for now!). 14 | 15 | **Basic Example** 16 | 17 | Each entity to be read or written to a spreadsheet needs to have a `ClassMap` defining the relationship between the class's properties and the spreadsheet. A couple notes on the basics: 18 | + Classes being mapped must have either a parameterless constructor or a constructor with optional arguments. 19 | + Indexes are optional. When reading, OpenSpreadsheet will attempt to match the spreadsheet header with the defined mapping name, or the property name if not defined. For writing, the mapping order will be used unless the index is explicitly defined. 20 | + The name map is optional. When reading, the name is used to match a property to a header name if not index is defined. When writing, the name will provide the header, defaulting to the property name. 21 | 22 | Most configuration properties have both a read and write version, if applicable. If you need to a class to have different mappings for reading and writing operations, simply use the appropriate map method. 23 | 24 | ```c# 25 | public class TestClassMap : ClassMap 26 | { 27 | public TestClassMap() 28 | { 29 | Map(x => x.Surname).Index(1).Name("Employee Last Name"); 30 | Map(x => x.GivenName).Index(2).Name("Employee First Name"); 31 | Map(x => x.Id).Index(3).Name("Employee Id"); 32 | Map(x => x.Address).Index(4).IgnoreWrite(true); 33 | Map(x => x.SSN).IndexRead(10).IndexWrite(5).CustomNumberFormat("000-00-0000"); 34 | Map(x => x.Amount).Index(6).Style(new ColumnStyle() { NumberFormat = OpenXmlNumberingFormat.Accounting }); 35 | } 36 | } 37 | ```` 38 |
39 | 40 | **Constants and Defaults** 41 | If you need to supply a constant value to a property during reading or you'd like to write a constant value (with or without an associated property), use the `Constant` map. 42 | 43 | If you need to supply a fallback value for null values, use the `Default` map. 44 | 45 | ```c# 46 | public class TestClassMap : ClassMap 47 | { 48 | public TestClassMap() 49 | { 50 | Map().Index(1).Name("Date").ConstantWrite(DateTime.Today.ToString()); 51 | Map(x => x.Id).Index(2).Name("Employee Id").Default(0); 52 | } 53 | } 54 | ```` 55 |
56 | 57 | **Column Styles** 58 | 59 | In order to customize the appearance of a style, simply create a new `ColumnStyle` instance and map it to the property using the `Style` method. If an explicit `ColumnStyle` is not specified, a default instance will be used. 60 | 61 | ```c# 62 | public class TestClassMap : ClassMap 63 | { 64 | public TestClassMap() 65 | { 66 | var columnStyle = new ColumnStyle() 67 | { 68 | BackgroundColor = Color.Aquamarine, 69 | BackgroundPatternType = DocumentFormat.OpenXml.Spreadsheet.PatternValues.Solid, 70 | BorderColor = Color.Red, 71 | BorderPlacement = BorderPlacement.Outside, 72 | BorderStyle = DocumentFormat.OpenXml.Spreadsheet.BorderStyleValues.Thin, 73 | Font = new Font("Arial", 14, FontStyle.Italic), 74 | ForegroundColor = Color.White, 75 | HoizontalAlignment = DocumentFormat.OpenXml.Spreadsheet.HorizontalAlignmentValues.Center, 76 | NumberFormat = OpenXmlNumberingFormat.Currency, 77 | VerticalAlignment = DocumentFormat.OpenXml.Spreadsheet.VerticalAlignmentValues.Center 78 | }; 79 | 80 | Map(x => x.Amount).Index(1).Style(columnStyle); 81 | Map(x => x.SSN).Index(2).Style(new ColumnStyle() { CustomNumberFormat = "000-00-0000" }); 82 | } 83 | } 84 | ``` 85 |
86 | 87 | **Data Conversions** 88 | 89 | Sometimes you need to provide some additional logic to accurately map between your spreadsheet and your class instance. In these cases, use the `ReadUsing` and `WriteUsing` to provide a delegate to be used for the mapping operation. The `ReadUsing` delegate takes a `ReaderRow` for its input parameter, which allows you to retrieve data from any cell within the row by using its header name or column index. The `WriteUsing` delegate takes the class instance as its input parameter. 90 | 91 | In the example blow, the `ClassMap` contains a map for the boolean property `IsExpired`. During reading, the value of `IsExpired` is determined by comparing the current date a date contained in a cell with a header named "ExpirationDate". When writing, the value written to the cell is 'T' or 'F' rather than the default `IsExpired.ToString()` of `True` or `False`. 92 | 93 | ```c# 94 | public class TestClassMap : ClassMap 95 | { 96 | public TestClassMap() 97 | { 98 | Map(x => x.IsExpired) 99 | .ReadUsing(row => 100 | { 101 | var expirationTextValue = row.GetCellValue("ExpirationDate"); 102 | var expirationDate = DateTime.Parse(expirationTextValue); 103 | return expirationDate < DateTime.Now; 104 | }) 105 | .WriteUsing(x => x.IsExpired ? "T" : "F"); 106 | } 107 | } 108 | ```` 109 | 110 | 111 | ### Writing 112 | 113 | To write data to a new worksheet, simply call the WriteWorksheet method from your Spreadsheet, providing the type of object to be written and its associtiated map. If you want more fine-grained control over the write operation, have your Spreadsheet create a new WorksheetWriter. 114 | 115 | ```c# 116 | using (var spreadsheet = new Spreadsheet(filepath)) 117 | { 118 | // write all records from the Spreadsheet (uses a WorksheetWriter behind the scenes) 119 | spreadsheet.WriteWorksheet("Sheet2", records); 120 | 121 | // write all records using an explicit WorksheetWriter 122 | using (var writer = spreadsheet.CreateWorksheetWriter("Sheet3")) 123 | { 124 | writer.WriteRecords(records); 125 | } 126 | 127 | // write individual records from the WorksheetWriter 128 | using (var writer = spreadsheet.CreateWorksheetWriter("Sheet1", 0)) 129 | { 130 | writer.WriteHeader(); 131 | writer.SkipRows(3); 132 | writer.WriteRecord(new TestClass() { TestData = "first row" }); 133 | writer.WriteRecord(new TestClass() { TestData = "second row" }); 134 | writer.WriteRecord(new TestClass() { TestData = "third row" }); 135 | writer.SkipRow(); 136 | writer.WriteRecord(new TestClass() { TestData = "fourth row" }); 137 | } 138 | } 139 | ``` 140 | 141 | To apply general worksheet styles, create a new WorksheetStyle instance and pass it as an argument to your write operations. Otherwise, a default WorksheetStyle instance will be used. 142 | 143 | ```c# 144 | var worksheetStyle = new WorksheetStyle() 145 | { 146 | HeaderBackgroundColor = Color.Chartreuse, 147 | HeaderBackgroundPatternType = DocumentFormat.OpenXml.Spreadsheet.PatternValues.Solid, 148 | HeaderFont = new Font("Comic Sans", 16, FontStyle.Strikeout), 149 | HeaderForegroundColor = Color.DarkBlue, 150 | HeaderHoizontalAlignment = DocumentFormat.OpenXml.Spreadsheet.HorizontalAlignmentValues.Center, 151 | HeaderRowIndex = 2, 152 | HeaderVerticalAlignment = DocumentFormat.OpenXml.Spreadsheet.VerticalAlignmentValues.Center, 153 | MaxColumnWidth = 30, 154 | MinColumnWidth = 10, 155 | ShouldAutoFilter = true, 156 | ShouldAutoFitColumns = true, 157 | ShouldFreezeTopRow = true, 158 | ShouldWriteHeaderRow = true, 159 | }; 160 | 161 | using (var spreadsheet = new Spreadsheet(filepath)) 162 | { 163 | spreadsheet.WriteWorksheet("Sheet1", records, worksheetStyle); 164 | } 165 | ``` 166 | 167 | ### Reading 168 | 169 | To read data from an existing worksheet, simply call the ReadWorksheet method from your Spreadsheet, providing the type of object to be written and its associtiated map. If you want more fine-grained control over the read operation, have your Spreadsheet create a new WorksheetReader. 170 | 171 | ```c# 172 | using (var spreadsheet = new Spreadsheet(filepath)) 173 | { 174 | // read all records from the Spreadsheet (uses a WorksheetReader behind the scenes) 175 | var recordsSheet1 = spreadsheet.ReadWorksheet("Sheet1"); 176 | 177 | // read all records using an explicit WorksheetReader 178 | using (var reader = spreadsheet.CreateWorksheetReader("Sheet2")) 179 | { 180 | var recordsSheet2 = reader.ReadRows(); 181 | } 182 | 183 | // read individual records 184 | using (var reader = spreadsheet.CreateWorksheetReader("Sheet3")) 185 | { 186 | var firstRow = reader.ReadRow(); 187 | var secondRow = reader.ReadRow(); 188 | reader.SkipRow(); 189 | var fourthRow = reader.ReadRow(); 190 | } 191 | } 192 | ``` 193 | 194 | ## Performance 195 | 196 | **Reading** 197 | 198 | OpenSpreadsheet is significantly faster and better on memory than ClosedXml, but is generally slower than EPPlus. For reading, all three libraries are pretty performant. 199 | 200 | | Library | Records | Fields | Runtime | Memory Used | 201 | | ------------- | ------------- | ------------- | ------------- | ------------- | 202 | | [ClosedXml](https://github.com/ClosedXML/ClosedXML) | 50,000 | 3 | 971.2 ms | 211.46 MB 203 | | [EPPlus](https://github.com/JanKallman/EPPlus) | 50,000 | 3 | 394.9 ms | 139.05 MB 204 | | [OpenSpreadsheet](https://github.com/FolkCoder/OpenSpreadsheet) | 50,000 | 3 | 745.7 ms | 121.14 MB 205 | | [ClosedXml](https://github.com/ClosedXML/ClosedXML) | 100,000 | 3 | 1,932.7 ms | 423.67 MB 206 | | [EPPlus](https://github.com/JanKallman/EPPlus) | 100,000 | 3 | 807.2 ms | 277.15 MB 207 | | [OpenSpreadsheet](https://github.com/FolkCoder/OpenSpreadsheet) | 100,000 | 3 | 1,502.6 ms | 241.69 MB 208 | | [ClosedXml](https://github.com/ClosedXML/ClosedXML) | 250,000 | 3 | 4,747.9 ms | 1044.93 MB 209 | | [EPPlus](https://github.com/JanKallman/EPPlus) | 250,000 | 3 | 2,003.8 ms| 686.58 MB 210 | | [OpenSpreadsheet](https://github.com/FolkCoder/OpenSpreadsheet) | 250,000 | 3 | 3,694.0 ms | 602.89 MB 211 | | [ClosedXml](https://github.com/ClosedXML/ClosedXML) | 500,000 | 3 | 113.359 ms | 2094.14 MB 212 | | [EPPlus](https://github.com/JanKallman/EPPlus) | 500,000 | 3 | 75.751 ms| 1372.95 MB 213 | | [OpenSpreadsheet](https://github.com/FolkCoder/OpenSpreadsheet) | 500,000 | 3 | 79.665 ms | 1205.57 MB 214 | 215 | 216 | **Writing** 217 | 218 | OpenSpreadsheet is significantly faster and memory-friendly than ClosedXml, and slightly more so than EPPlus. 219 | 220 | | Library | Records | Fields | Runtime | Memory Used | 221 | | ------------- | ------------- | ------------- | ------------- | ------------- | 222 | | [ClosedXml](https://github.com/ClosedXML/ClosedXML) | 50,000 | 30 | 12.013 s | 2459.94 MB 223 | | [EPPlus](https://github.com/JanKallman/EPPlus) | 50,000 | 30 | 3.351 s | 1039.68 MB 224 | | [OpenSpreadsheet](https://github.com/FolkCoder/OpenSpreadsheet) | 50,000 | 30 | 2.401 s | 1006.11 MB 225 | | [ClosedXml](https://github.com/ClosedXML/ClosedXML) | 100,000 | 30 | 23.908 s | 4928.38 MB 226 | | [EPPlus](https://github.com/JanKallman/EPPlus) | 100,000 | 30 | 6.658 s | 2053.81 MB 227 | | [OpenSpreadsheet](https://github.com/FolkCoder/OpenSpreadsheet) | 100,000 | 30 | 4.865 s | 2005.31 MB 228 | | [ClosedXml](https://github.com/ClosedXML/ClosedXML) | 250,000 | 30 | 59.999 s | 12027.75 MB 229 | | [EPPlus](https://github.com/JanKallman/EPPlus) | 250,000 | 30 | 16.526 s | 5041.11 MB 230 | | [OpenSpreadsheet](https://github.com/FolkCoder/OpenSpreadsheet) | 250,000 | 30 | 11.997 s | 4815.44 MB 231 | 232 | 233 | ## Future Plans 234 | + Automatic class mapping 235 | + Support for dynamic and anonymous types 236 | + Better handling of duplicate header names for reading 237 | + Greatly improve accuracy and coverage of automated tests and ClassMap validations 238 | + Allow default worksheet style for entire spreadsheet 239 | + Provide override for ReadWorksheet to accept tab position index as well as name -------------------------------------------------------------------------------- /src/Benchmarks/Benchmarks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | Exe 6 | netcoreapp2.1 7 | false 8 | 7.2 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | namespace Benchmarks 2 | { 3 | using BenchmarkDotNet.Running; 4 | using Benchmarks.Tests; 5 | 6 | public static class Program 7 | { 8 | private static void Main() 9 | { 10 | var read = BenchmarkRunner.Run(); 11 | var write = BenchmarkRunner.Run(); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Benchmarks/Properties/PublishProfiles/FolderProfile.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | FileSystem 8 | Release 9 | Any CPU 10 | netcoreapp2.1 11 | bin\Release\netcoreapp2.1\publish\ 12 | 13 | -------------------------------------------------------------------------------- /src/Benchmarks/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Benchmarks": { 4 | "commandName": "Project" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /src/Benchmarks/Tests/BenchmarkRead.cs: -------------------------------------------------------------------------------- 1 | namespace Benchmarks.Tests 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using BenchmarkDotNet.Attributes; 8 | using ClosedXML.Excel; 9 | using OfficeOpenXml; 10 | using OpenSpreadsheet; 11 | using OpenSpreadsheet.Configuration; 12 | 13 | [MemoryDiagnoser] 14 | public class BenchmarkRead 15 | { 16 | private const string worksheetName = "test sheet"; 17 | private string inputPath; 18 | 19 | [Params(50000, 100000, 250000, 500000)] 20 | public int RecordCount { get; set; } 21 | 22 | [GlobalSetup] 23 | public void GlobalSetup() 24 | { 25 | this.inputPath = Path.GetTempPath() + Guid.NewGuid().ToString() + ".xlsx"; 26 | var records = CreateRecords(this.RecordCount); 27 | using (var spreadsheet = new Spreadsheet(this.inputPath)) 28 | { 29 | spreadsheet.WriteWorksheet(worksheetName, records); 30 | } 31 | } 32 | 33 | [GlobalCleanup] 34 | public void GlobalCleanup() => File.Delete(this.inputPath); 35 | 36 | [Benchmark] 37 | public void TestClosedXml() 38 | { 39 | var records = new List(); 40 | using (var workbook = new XLWorkbook(this.inputPath, XLEventTracking.Disabled)) 41 | { 42 | workbook.TryGetWorksheet(worksheetName, out IXLWorksheet worksheet); 43 | 44 | for (int i = 0; i < this.RecordCount; i++) 45 | { 46 | int row = i + 1; 47 | records.Add(new TestRecord 48 | { 49 | TestData1 = (string)worksheet.Cell(row, 1).Value, 50 | TestData2 = (string)worksheet.Cell(row, 2).Value, 51 | TestData3 = (string)worksheet.Cell(row, 3).Value 52 | }); 53 | } 54 | } 55 | } 56 | 57 | [Benchmark] 58 | public void TestEPPlus() 59 | { 60 | var records = new List(); 61 | var fileInfo = new FileInfo(this.inputPath); 62 | using (var excelPackage = new ExcelPackage(fileInfo)) 63 | { 64 | var worksheet = excelPackage.Workbook.Worksheets[worksheetName]; 65 | 66 | for (int i = 0; i < this.RecordCount; i++) 67 | { 68 | int row = i + 1; 69 | records.Add(new TestRecord 70 | { 71 | TestData1 = (string)worksheet.Cells[row, 1].Value, 72 | TestData2 = (string)worksheet.Cells[row, 2].Value, 73 | TestData3 = (string)worksheet.Cells[row, 3].Value 74 | }); 75 | } 76 | } 77 | } 78 | 79 | [Benchmark] 80 | public void TestOpenSpreadsheet() 81 | { 82 | using (var spreadsheet = new Spreadsheet(this.inputPath)) 83 | { 84 | var records = spreadsheet.ReadWorksheet(worksheetName).ToList(); 85 | } 86 | } 87 | 88 | private class TestClassMap : ClassMap 89 | { 90 | public TestClassMap() 91 | { 92 | base.Map(x => x.TestData1).Index(1); 93 | base.Map(x => x.TestData2).Index(2); 94 | base.Map(x => x.TestData3).Index(3); 95 | } 96 | } 97 | 98 | private class TestRecord 99 | { 100 | public string TestData1 { get; set; } 101 | public string TestData2 { get; set; } 102 | public string TestData3 { get; set; } 103 | } 104 | 105 | private static IEnumerable CreateRecords(int amount) 106 | { 107 | for (int i = 0; i < amount; i++) 108 | { 109 | yield return new TestRecord() 110 | { 111 | TestData1 = "sadassdgsdfsdfsgsdfsdfds", 112 | TestData2 = "afasdljfsdlgjsdljfsldjfl", 113 | TestData3 = "g;fdgjlsdfhsdfiefndslc k", 114 | }; 115 | } 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /src/Benchmarks/Tests/BenchmarkWrite.cs: -------------------------------------------------------------------------------- 1 | namespace Benchmarks.Tests 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using BenchmarkDotNet.Attributes; 8 | using ClosedXML.Excel; 9 | using OfficeOpenXml; 10 | using OpenSpreadsheet; 11 | using OpenSpreadsheet.Configuration; 12 | 13 | [MemoryDiagnoser] 14 | public class BenchmarkWrite 15 | { 16 | private const uint columnCount = 30; 17 | private IList records; 18 | 19 | [Params(50000, 100000, 250000)] 20 | public int RecordCount { get; set; } 21 | 22 | [GlobalSetup] 23 | public void GlobalSetup() => this.records = CreateRecords(this.RecordCount).ToList(); 24 | 25 | [Benchmark] 26 | public void TestClosedXml() 27 | { 28 | string outputPath = Path.GetTempPath() + Guid.NewGuid().ToString() + ".xlsx"; 29 | using (var workbook = new XLWorkbook(XLEventTracking.Disabled)) 30 | { 31 | var worksheet = workbook.Worksheets.Add("Test Sheet"); 32 | 33 | int row = 1; 34 | foreach (var record in this.records) 35 | { 36 | for (int i = 0; i < columnCount; i++) 37 | { 38 | int col = i + 1; 39 | worksheet.Cell(row, col).Value = record.TestData; 40 | } 41 | 42 | row++; 43 | } 44 | 45 | workbook.SaveAs(outputPath); 46 | } 47 | 48 | File.Delete(outputPath); 49 | } 50 | 51 | [Benchmark] 52 | public void TestEPPlus() 53 | { 54 | string outputPath = Path.GetTempPath() + Guid.NewGuid().ToString() + ".xlsx"; 55 | var fileInfo = new FileInfo(outputPath); 56 | using (var excelPackage = new ExcelPackage(fileInfo)) 57 | { 58 | var worksheet = excelPackage.Workbook.Worksheets.Add("test sheet"); 59 | 60 | int row = 1; 61 | foreach (var record in this.records) 62 | { 63 | for (int i = 0; i < columnCount; i++) 64 | { 65 | int col = i + 1; 66 | worksheet.Cells[row, col].Value = record.TestData; 67 | } 68 | 69 | row++; 70 | } 71 | 72 | excelPackage.Save(); 73 | } 74 | 75 | File.Delete(outputPath); 76 | } 77 | 78 | [Benchmark] 79 | public void TestOpenSpreadsheet() 80 | { 81 | string outputPath = Path.GetTempPath() + Guid.NewGuid().ToString() + ".xlsx"; 82 | using (var spreadsheet = new Spreadsheet(outputPath)) 83 | { 84 | spreadsheet.WriteWorksheet("test sheet", this.records); 85 | } 86 | 87 | File.Delete(outputPath); 88 | } 89 | 90 | private class TestClassMap : ClassMap 91 | { 92 | public TestClassMap() 93 | { 94 | for (uint i = 0; i < columnCount; i++) 95 | { 96 | uint colIndex = i + 1; 97 | base.Map(x => x.TestData).Index(colIndex).IgnoreRead(true); 98 | } 99 | } 100 | } 101 | 102 | private class TestRecord 103 | { 104 | public string TestData { get; set; } = "sadassdgsdfsdfsgsdfsdfds"; 105 | } 106 | 107 | private static IEnumerable CreateRecords(int amount) 108 | { 109 | for (int i = 0; i < amount; i++) 110 | { 111 | yield return new TestRecord(); 112 | } 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /src/OpenSpreadsheet.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.168 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenSpreadsheet", "OpenSpreadsheet\OpenSpreadsheet.csproj", "{B4603728-ADF4-467A-808A-F0A90429C2DA}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "Benchmarks\Benchmarks.csproj", "{0CCB698A-CB56-4CA5-8CE2-F75C6C2BCBB8}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{357E04AF-32DE-44DE-82A0-6ED37F7B5C46}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {B4603728-ADF4-467A-808A-F0A90429C2DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {B4603728-ADF4-467A-808A-F0A90429C2DA}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {B4603728-ADF4-467A-808A-F0A90429C2DA}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {B4603728-ADF4-467A-808A-F0A90429C2DA}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {0CCB698A-CB56-4CA5-8CE2-F75C6C2BCBB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {0CCB698A-CB56-4CA5-8CE2-F75C6C2BCBB8}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {0CCB698A-CB56-4CA5-8CE2-F75C6C2BCBB8}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {0CCB698A-CB56-4CA5-8CE2-F75C6C2BCBB8}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {357E04AF-32DE-44DE-82A0-6ED37F7B5C46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {357E04AF-32DE-44DE-82A0-6ED37F7B5C46}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {357E04AF-32DE-44DE-82A0-6ED37F7B5C46}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {357E04AF-32DE-44DE-82A0-6ED37F7B5C46}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {D3FEAC81-A24F-403E-B85E-0F3A5D92DC79} 36 | EndGlobalSection 37 | GlobalSection(Performance) = preSolution 38 | HasPerformanceSessions = true 39 | EndGlobalSection 40 | GlobalSection(Performance) = preSolution 41 | HasPerformanceSessions = true 42 | EndGlobalSection 43 | GlobalSection(Performance) = preSolution 44 | HasPerformanceSessions = true 45 | EndGlobalSection 46 | EndGlobal 47 | -------------------------------------------------------------------------------- /src/OpenSpreadsheet/BidirectionalDictionary.cs: -------------------------------------------------------------------------------- 1 | namespace OpenSpreadsheet 2 | { 3 | using System.Collections.Generic; 4 | 5 | /// 6 | /// Extends the collection to support fast reverse lookups. 7 | /// 8 | /// The dictionary's key type. 9 | /// The dictionary's value type. 10 | public sealed class BidirectionalDictionary : Dictionary 11 | { 12 | private readonly Dictionary reverseLookup = new Dictionary(); 13 | 14 | /// 15 | /// Adds a new key-value pair to the collection. 16 | /// 17 | /// The key to be added. 18 | /// The value to be added. 19 | public new void Add(TKey key, TValue value) 20 | { 21 | base.Add(key, value); 22 | this.reverseLookup.Add(value, key); 23 | } 24 | 25 | /// 26 | /// Attempts to retrieve a key associated with the provided value. 27 | /// 28 | /// The value being queried. 29 | /// An output parameter that will contain the found key, if any. 30 | /// A value indicating whether a key associated with the provided value was identified. 31 | public bool TryGetKey(TValue value, out TKey key) 32 | { 33 | if (this.reverseLookup.TryGetValue(value, out key)) 34 | { 35 | return true; 36 | } 37 | 38 | key = default; 39 | return false; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/OpenSpreadsheet/Configuration/ClassMap.cs: -------------------------------------------------------------------------------- 1 | namespace OpenSpreadsheet.Configuration 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq.Expressions; 6 | using System.Reflection; 7 | 8 | /// 9 | /// Maps class properties to spreadsheet fields. 10 | /// 11 | /// The of class to map. 12 | public abstract class ClassMap where TClass : class 13 | { 14 | /// 15 | /// Gets a collection of the class map individual property mappings. 16 | /// 17 | public virtual IList> PropertyMaps { get; } = new List>(); 18 | 19 | /// 20 | /// Maps a property to the class map. 21 | /// 22 | /// The property type to be mapped. 23 | /// The property to be mapped. 24 | /// A property map associated with the property. 25 | public virtual PropertyMap Map(Expression> property) 26 | { 27 | if (property == null) 28 | { 29 | throw new ArgumentNullException(nameof(property)); 30 | } 31 | 32 | if (property.Body is UnaryExpression unaryExp) 33 | { 34 | if (unaryExp.Operand is MemberExpression memberExp) 35 | { 36 | var propertyInfo = (PropertyInfo)memberExp.Member; 37 | var propertyMap = new PropertyMap(propertyInfo); 38 | this.PropertyMaps.Add(propertyMap); 39 | return propertyMap; 40 | } 41 | } 42 | else if (property.Body is MemberExpression memberExp) 43 | { 44 | var propertyInfo = (PropertyInfo)memberExp.Member; 45 | var propertyMap = new PropertyMap(propertyInfo); 46 | this.PropertyMaps.Add(propertyMap); 47 | return propertyMap; 48 | } 49 | 50 | throw new ArgumentException($"The expression doesn't indicate a valid property. [ {property} ]"); 51 | } 52 | 53 | /// 54 | /// Maps a default property map to the class map; used for constants and values not mapped to a particular class property. 55 | /// 56 | /// A default property map. 57 | public virtual PropertyMap Map() 58 | { 59 | var propertyMap = new PropertyMap(); 60 | this.PropertyMaps.Add(propertyMap); 61 | return propertyMap; 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/OpenSpreadsheet/Configuration/ColumnStyle.cs: -------------------------------------------------------------------------------- 1 | namespace OpenSpreadsheet.Configuration 2 | { 3 | using System.Drawing; 4 | using OpenSpreadsheet.Enums; 5 | 6 | using OpenXml = DocumentFormat.OpenXml.Spreadsheet; 7 | 8 | /// 9 | /// Encapsulates properties associated with a spreadsheet column. 10 | /// 11 | public class ColumnStyle 12 | { 13 | private string customNumberFormat; 14 | private OpenXmlNumberingFormat numberFormat; 15 | 16 | /// 17 | /// Gets or sets the background color. 18 | /// 19 | public virtual Color BackgroundColor { get; set; } = Color.Transparent; 20 | 21 | /// 22 | /// Gets or sets the background pattern type. 23 | /// 24 | public virtual OpenXml.PatternValues BackgroundPatternType { get; set; } 25 | 26 | /// 27 | /// Gets or sets the border color. 28 | /// 29 | public virtual Color BorderColor { get; set; } = Color.Black; 30 | 31 | /// 32 | /// Gets or sets the border placement. 33 | /// 34 | public virtual BorderPlacement BorderPlacement { get; set; } 35 | 36 | /// 37 | /// Gets or sets the border style. 38 | /// 39 | public virtual OpenXml.BorderStyleValues BorderStyle { get; set; } 40 | 41 | /// 42 | /// Gets or sets a custom numbering format. 43 | /// 44 | public string CustomNumberFormat 45 | { 46 | get => this.customNumberFormat; 47 | set 48 | { 49 | this.customNumberFormat = value; 50 | this.IsNumberFormatSpecified = true; 51 | } 52 | } 53 | 54 | /// 55 | /// Gets or sets the font. 56 | /// 57 | public virtual Font Font { get; set; } = new Font("Calibri", 11, FontStyle.Regular); 58 | 59 | /// 60 | /// Gets or sets the text color. 61 | /// 62 | public virtual Color ForegroundColor { get; set; } = Color.Black; 63 | 64 | /// 65 | /// Gets or sets the horizontal alignment. 66 | /// 67 | public virtual OpenXml.HorizontalAlignmentValues HoizontalAlignment { get; set; } 68 | 69 | /// 70 | /// Gets a value indicating whether a number format has been explicitly specified. 71 | /// 72 | public bool IsNumberFormatSpecified { get; private set; } 73 | 74 | /// 75 | /// Gets or sets a number format. 76 | /// 77 | public OpenXmlNumberingFormat NumberFormat 78 | { 79 | get => this.numberFormat; 80 | set 81 | { 82 | this.numberFormat = value; 83 | this.IsNumberFormatSpecified = true; 84 | } 85 | } 86 | 87 | /// 88 | /// Gets or sets the vertical alignment. 89 | /// 90 | public virtual OpenXml.VerticalAlignmentValues VerticalAlignment { get; set; } = OpenXml.VerticalAlignmentValues.Center; 91 | } 92 | } -------------------------------------------------------------------------------- /src/OpenSpreadsheet/Configuration/ConfigurationValidator.cs: -------------------------------------------------------------------------------- 1 | namespace OpenSpreadsheet.Configuration 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | /// 9 | /// Provides methods to validate configuration properties. 10 | /// 11 | public class ConfigurationValidator 12 | where TClass : class 13 | where TClassMap : ClassMap 14 | { 15 | private readonly ClassMap classMap; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// 21 | /// 22 | public ConfigurationValidator() => this.classMap = Activator.CreateInstance(); 23 | 24 | /// 25 | /// Gets a collection of validation errors. 26 | /// 27 | public List Errors { get; } = new List(); 28 | 29 | /// 30 | /// Gets a value indicating whether the validator has errors. 31 | /// 32 | public bool HasErrors => this.Errors.Count > 0; 33 | 34 | /// 35 | /// Validates the property maps. 36 | /// 37 | public void Validate() 38 | { 39 | this.Errors.Clear(); 40 | 41 | // read 42 | this.ValidateIndexesAreUnique(this.classMap.PropertyMaps.Where(x => !x.PropertyData.IgnoreRead && x.PropertyData.IndexRead > 0).Select(x => x.PropertyData.IndexRead), ConfigurationType.Read); 43 | this.ValidateReadProperties(this.classMap.PropertyMaps.Where(x => !x.PropertyData.IgnoreRead && x.PropertyData.Property != null)); 44 | foreach (var map in this.classMap.PropertyMaps.Where(x => !x.PropertyData.IgnoreRead)) 45 | { 46 | this.ValidateConstant(map.PropertyData.ConstantRead, map); 47 | this.ValidateDefault(map.PropertyData.DefaultRead, map); 48 | this.ValidateIndexWithinExcelMaxRange(map.PropertyData.IndexRead, map, ConfigurationType.Read); 49 | } 50 | 51 | // write 52 | this.ValidateIndexesAreUnique(this.classMap.PropertyMaps.Where(x => !x.PropertyData.IgnoreWrite && x.PropertyData.IndexWrite > 0).Select(x => x.PropertyData.IndexWrite), ConfigurationType.Write); 53 | foreach (var map in this.classMap.PropertyMaps.Where(x => !x.PropertyData.IgnoreWrite)) 54 | { 55 | this.ValidateIndexWithinExcelMaxRange(map.PropertyData.IndexWrite, map, ConfigurationType.Write); 56 | this.ValidateHeaderNameWithinExcelMaxLength(map.PropertyData.NameWrite, map); 57 | } 58 | 59 | this.Errors.OrderBy(x => x.Message); 60 | } 61 | 62 | private void ValidateConstant(object constantValue, PropertyMap map) 63 | { 64 | if (constantValue == null) 65 | { 66 | return; 67 | } 68 | 69 | // constant is not mapped to a particular property 70 | if (map.PropertyData.Property == null) 71 | { 72 | return; 73 | } 74 | 75 | var propertyType = Nullable.GetUnderlyingType(map.PropertyData.Property.PropertyType) ?? map.PropertyData.Property.PropertyType; 76 | if (constantValue.GetType() != propertyType) 77 | { 78 | this.Errors.Add(new ArgumentException($"Constant of type '{constantValue.GetType().FullName}' does not match member of type '{map.PropertyData.Property.PropertyType.Name}'.")); 79 | } 80 | } 81 | 82 | private void ValidateDefault(object defaultValue, PropertyMap map) 83 | { 84 | if (defaultValue == null) 85 | { 86 | return; 87 | } 88 | 89 | var propertyType = Nullable.GetUnderlyingType(map.PropertyData.Property.PropertyType) ?? map.PropertyData.Property.PropertyType; 90 | if (defaultValue.GetType() != propertyType) 91 | { 92 | this.Errors.Add(new ArgumentException($"Default of type '{defaultValue.GetType().FullName}' does not match member of type '{map.PropertyData.Property.PropertyType.Name}'.")); 93 | } 94 | } 95 | 96 | private void ValidateHeaderNameWithinExcelMaxLength(string name, PropertyMap map) 97 | { 98 | const int maxHeaderLength = 255; 99 | 100 | string headerName = name ?? map.PropertyData.Property.Name; 101 | if (headerName.Length > maxHeaderLength) 102 | { 103 | this.Errors.Add(new ArgumentException($"Property '{map.PropertyData.Property.Name}' has a header name '{headerName}' that is longer than the maximum length allowed by Excel ({maxHeaderLength.ToString()}).")); 104 | } 105 | } 106 | 107 | private void ValidateIndexesAreUnique(IEnumerable indexes, ConfigurationType configurationType) 108 | { 109 | var uniqueIndexes = new HashSet(); 110 | foreach (var index in indexes) 111 | { 112 | if (uniqueIndexes.Contains(index)) 113 | { 114 | this.Errors.Add(new ArgumentException($"Column index {index} is defined for multiple {configurationType.ToString().ToLower()} properties.")); 115 | } 116 | else 117 | { 118 | uniqueIndexes.Add(index); 119 | } 120 | } 121 | } 122 | 123 | private void ValidateIndexWithinExcelMaxRange(uint index, PropertyMap map, ConfigurationType configurationType) 124 | { 125 | const int maxColumnIndex = 16384; 126 | 127 | if (index > maxColumnIndex) 128 | { 129 | this.Errors.Add(new ArgumentException($"{configurationType.ToString()} property '{map.PropertyData.Property.Name}' has a defined column index '{index.ToString()}' that is greater than the maximum number of columns allowed by Excel ({maxColumnIndex.ToString()}).")); 130 | } 131 | } 132 | 133 | private void ValidateReadProperties(IEnumerable> propertyMaps) 134 | { 135 | var props = new HashSet(); 136 | foreach (var map in propertyMaps) 137 | { 138 | if (props.Contains(map.PropertyData.Property)) 139 | { 140 | this.Errors.Add(new ArgumentException($"Read property '{map.PropertyData.Property.Name}' is mapped to more than one column.")); 141 | } 142 | else 143 | { 144 | props.Add(map.PropertyData.Property); 145 | } 146 | } 147 | } 148 | 149 | private enum ConfigurationType 150 | { 151 | Read, 152 | Write 153 | } 154 | } 155 | } -------------------------------------------------------------------------------- /src/OpenSpreadsheet/Configuration/PropertyMap.cs: -------------------------------------------------------------------------------- 1 | namespace OpenSpreadsheet.Configuration 2 | { 3 | using System; 4 | using System.Linq.Expressions; 5 | using System.Reflection; 6 | 7 | using Enums; 8 | 9 | /// 10 | /// Mapping info between a class property and a spreadsheet column. 11 | /// 12 | public class PropertyMap where TClass : class 13 | { 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | public PropertyMap() => this.PropertyData = new PropertyMapData(); 18 | 19 | /// 20 | /// Initializes a new instance of the class. 21 | /// 22 | /// The property data associated with the map. 23 | public PropertyMap(PropertyInfo propertyInfo) => this.PropertyData = new PropertyMapData(propertyInfo); 24 | 25 | /// 26 | /// Gets the data associated with the property map. 27 | /// 28 | public PropertyMapData PropertyData { get; } 29 | 30 | /// 31 | /// Sets the column type for both read and write operations. 32 | /// 33 | /// The column type to be applied. 34 | /// The changed PropertyMap. 35 | public virtual PropertyMap ColumnType(ColumnType columnType) 36 | { 37 | this.PropertyData.ColumnType = columnType; 38 | return this; 39 | } 40 | 41 | /// 42 | /// Sets a constant value to be used for both read and write operations. 43 | /// 44 | /// The constant value to be used. When reading, the value must be the same type as the mapped property. 45 | /// The changed PropertyMap. 46 | public virtual PropertyMap Constant(object value) 47 | { 48 | this.PropertyData.Constant = value; 49 | this.PropertyData.ConstantRead = value; 50 | this.PropertyData.ConstantWrite = value; 51 | 52 | return this; 53 | } 54 | 55 | /// 56 | /// Sets a constant value to be used for read operations. 57 | /// 58 | /// The constant value to be used. The value must be of the same type as the mapped property. 59 | /// The changed PropertyMap. 60 | public virtual PropertyMap ConstantRead(object value) 61 | { 62 | this.PropertyData.ConstantRead = value; 63 | return this; 64 | } 65 | 66 | /// 67 | /// Sets a constant value to be used for write operations. 68 | /// 69 | /// The constant value to be used. 70 | /// The changed PropertyMap. 71 | public virtual PropertyMap ConstantWrite(object value) 72 | { 73 | this.PropertyData.ConstantWrite = value; 74 | return this; 75 | } 76 | 77 | /// 78 | /// Sets a default value to be used for read and write operations when the property value is null. 79 | /// 80 | /// The default value to be used. When reading, the value must be the same type as the mapped property. 81 | /// The changed PropertyMap. 82 | public virtual PropertyMap Default(object value) 83 | { 84 | this.PropertyData.Default = value; 85 | this.PropertyData.DefaultRead = value; 86 | this.PropertyData.DefaultWrite = value; 87 | 88 | return this; 89 | } 90 | 91 | /// 92 | /// Sets a default value to be used for read operations when the property value is null. 93 | /// 94 | /// The default value to be used. The value must be the same type as the mapped property. 95 | /// The changed PropertyMap. 96 | public virtual PropertyMap DefaultRead(object value) 97 | { 98 | this.PropertyData.DefaultRead = value; 99 | return this; 100 | } 101 | 102 | /// 103 | /// Sets a default value to be used for write operations when the property value is null. 104 | /// 105 | /// The default value to be used. 106 | /// The changed PropertyMap. 107 | public virtual PropertyMap DefaultWrite(object value) 108 | { 109 | this.PropertyData.DefaultWrite = value; 110 | return this; 111 | } 112 | 113 | /// 114 | /// Sets a value indicating whether the associated property should be ignored on read and write operations. 115 | /// 116 | /// A value indicating whether the associated property should be ignored. 117 | /// The changed PropertyMap. 118 | public virtual PropertyMap Ignore(bool value) 119 | { 120 | this.PropertyData.Ignore = value; 121 | this.PropertyData.IgnoreRead = value; 122 | this.PropertyData.IgnoreWrite = value; 123 | 124 | return this; 125 | } 126 | 127 | /// 128 | /// Sets a value indicating whether the associated property should be ignored on read operations. 129 | /// 130 | /// A value indicating whether the associated property should be ignored. 131 | /// The changed PropertyMap. 132 | public virtual PropertyMap IgnoreRead(bool value) 133 | { 134 | this.PropertyData.IgnoreRead = value; 135 | return this; 136 | } 137 | 138 | /// 139 | /// Sets a value indicating whether the associated property should be ignored on write operations. 140 | /// 141 | /// A value indicating whether the associated property should be ignored. 142 | /// The changed PropertyMap. 143 | public virtual PropertyMap IgnoreWrite(bool value) 144 | { 145 | this.PropertyData.IgnoreWrite = value; 146 | return this; 147 | } 148 | 149 | /// 150 | /// Sets the one-based column index of the associated property to be used for read and write operations. 151 | /// 152 | /// The property's column index. 153 | /// The changed PropertyMap. 154 | public virtual PropertyMap Index(uint index) 155 | { 156 | this.PropertyData.Index = index; 157 | this.PropertyData.IndexRead = index; 158 | this.PropertyData.IndexWrite = index; 159 | 160 | return this; 161 | } 162 | 163 | /// 164 | /// Sets the one-based column index of the associated property to be used for read operations. 165 | /// 166 | /// The property's column index. 167 | /// The changed PropertyMap. 168 | public virtual PropertyMap IndexRead(uint index) 169 | { 170 | this.PropertyData.IndexRead = index; 171 | return this; 172 | } 173 | 174 | /// 175 | /// Sets the one-based column index of the associated property to be used for write operations. 176 | /// 177 | /// The property's column index. 178 | /// The changed PropertyMap. 179 | public virtual PropertyMap IndexWrite(uint index) 180 | { 181 | this.PropertyData.IndexWrite = index; 182 | return this; 183 | } 184 | 185 | /// 186 | /// Sets the column header name to be used for read and write operations. 187 | /// 188 | /// The property's column header name. 189 | /// The changed PropertyMap. 190 | public virtual PropertyMap Name(string name) 191 | { 192 | this.PropertyData.Name = name; 193 | this.PropertyData.NameRead = name; 194 | this.PropertyData.NameWrite = name; 195 | 196 | return this; 197 | } 198 | 199 | /// 200 | /// Sets the column header name to be used for read operations. 201 | /// 202 | /// The property's column header name. 203 | /// The changed PropertyMap. 204 | public virtual PropertyMap NameRead(string name) 205 | { 206 | this.PropertyData.NameRead = name; 207 | return this; 208 | } 209 | 210 | /// 211 | /// Sets the column header name to be used for read operations. 212 | /// 213 | /// The property's column header name. 214 | /// The changed PropertyMap. 215 | public virtual PropertyMap NameWrite(string name) 216 | { 217 | this.PropertyData.NameWrite = name; 218 | return this; 219 | } 220 | 221 | /// 222 | /// Sets the delegate to be used to resolve the property's value when reading. 223 | /// 224 | /// The expression to be set. 225 | /// The changed PropertyMap. 226 | public virtual PropertyMap ReadUsing(Func convertExpression) 227 | { 228 | Expression> expression = x => convertExpression(x); 229 | this.PropertyData.ReadUsing = expression.Compile(); 230 | return this; 231 | } 232 | 233 | /// 234 | /// Sets the column style. 235 | /// 236 | /// The style to be applied. 237 | /// The changed PropertyMap. 238 | public virtual PropertyMap Style(ColumnStyle columnStyle) 239 | { 240 | this.PropertyData.Style = columnStyle; 241 | return this; 242 | } 243 | 244 | /// 245 | /// Sets the delegate to be used to resolve the property's value when writing. 246 | /// 247 | /// The expression to be set. 248 | /// The changed PropertyMap. 249 | public virtual PropertyMap WriteUsing(Func convertExpression) 250 | { 251 | Expression> expression = x => convertExpression(x); 252 | this.PropertyData.WriteUsing = expression.Compile(); 253 | return this; 254 | } 255 | } 256 | } -------------------------------------------------------------------------------- /src/OpenSpreadsheet/Configuration/PropertyMapData.cs: -------------------------------------------------------------------------------- 1 | namespace OpenSpreadsheet.Configuration 2 | { 3 | using System; 4 | using System.Linq.Expressions; 5 | using System.Reflection; 6 | 7 | using OpenSpreadsheet.Enums; 8 | 9 | /// 10 | /// Encapsulates properties associated with an individual property map. 11 | /// 12 | public class PropertyMapData where TClass : class 13 | { 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | public PropertyMapData() { } 18 | 19 | /// 20 | /// Initializes a new instance of the class. 21 | /// 22 | /// The property data associated with the map. 23 | public PropertyMapData(PropertyInfo propertyInfo) => this.Property = propertyInfo; 24 | 25 | /// 26 | /// Gets or sets the map's column type. 27 | /// 28 | public virtual ColumnType ColumnType { get; set; } 29 | 30 | /// 31 | /// Gets or sets a constant value to be used for read and write operations. 32 | /// 33 | public virtual object Constant { get; set; } 34 | 35 | /// 36 | /// Gets or sets a constant value to be used for read operations. 37 | /// 38 | public virtual object ConstantRead { get; set; } 39 | 40 | /// 41 | /// Gets or sets a constant value to be used for write operations. 42 | /// 43 | public virtual object ConstantWrite { get; set; } 44 | 45 | /// 46 | /// Gets or sets a default value to be used for read and write operations when the property value is null. 47 | /// 48 | public virtual object Default { get; set; } 49 | 50 | /// 51 | /// Gets or sets a default value to be used for write operations when the property value is null. 52 | /// 53 | public virtual object DefaultRead { get; set; } 54 | 55 | /// 56 | /// Gets or sets a default value to be used for write operations when the property value is null. 57 | /// 58 | public virtual object DefaultWrite { get; set; } 59 | 60 | /// 61 | /// Gets or sets a value indicating whether the associated property should be ignored on read and write operations. 62 | /// 63 | public virtual bool Ignore { get; set; } 64 | 65 | /// 66 | /// Gets or sets a value indicating whether the associated property should be ignored on read operations. 67 | /// 68 | public virtual bool IgnoreRead { get; set; } 69 | 70 | /// 71 | /// Gets or sets a value indicating whether the associated property should be ignored on write operations. 72 | /// 73 | public virtual bool IgnoreWrite { get; set; } 74 | 75 | /// 76 | /// Sets the one-based column index of the associated property to be used for read and write operations. 77 | /// 78 | public virtual uint Index { get; set; } 79 | 80 | /// 81 | /// Sets the one-based column index of the associated property to be used for read operations. 82 | /// 83 | public virtual uint IndexRead { get; set; } 84 | 85 | /// 86 | /// Sets the one-based column index of the associated property to be used for write operations. 87 | /// 88 | public virtual uint IndexWrite { get; set; } 89 | 90 | /// 91 | /// Sets the column header name to be used for read and write operations. 92 | /// 93 | public virtual string Name { get; set; } 94 | 95 | /// 96 | /// Sets the column header name to be used for read operations. 97 | /// 98 | public virtual string NameRead { get; set; } 99 | 100 | /// 101 | /// Sets the column header name to be used for write operations. 102 | /// 103 | public virtual string NameWrite { get; set; } 104 | 105 | /// 106 | /// Gets the property data associated with the map. 107 | /// 108 | public virtual PropertyInfo Property { get; } 109 | 110 | /// 111 | /// Gets or sets the expression used to resolve the property value when reading. 112 | /// 113 | public virtual Func ReadUsing { get; set; } 114 | 115 | /// 116 | /// Gets or sets the column style. 117 | /// 118 | public virtual ColumnStyle Style { get; set; } = new ColumnStyle(); 119 | 120 | /// 121 | /// Gets or sets the expression used to resolve the property value when writing. 122 | /// 123 | public virtual Func WriteUsing { get; set; } 124 | } 125 | } -------------------------------------------------------------------------------- /src/OpenSpreadsheet/Configuration/StylesCollection.cs: -------------------------------------------------------------------------------- 1 | namespace OpenSpreadsheet.Configuration 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Drawing; 6 | using System.Linq; 7 | using OpenSpreadsheet.Enums; 8 | 9 | using OpenXml = DocumentFormat.OpenXml.Spreadsheet; 10 | 11 | /// 12 | /// Stores spreadsheet style definitions. 13 | /// 14 | /// For some elements, attributes must be present in a particular order in order to validate against the OpenXml schema. 15 | public class StylesCollection 16 | { 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | public StylesCollection() { } 21 | 22 | /// 23 | /// Gets a collection of borders and their associated stylehseet position index. 24 | /// 25 | public Dictionary Borders { get; } = new Dictionary(); 26 | 27 | /// 28 | /// Gets a collection of cell formats and their associated stylehseet position index. 29 | /// 30 | public Dictionary CellFormats { get; } = new Dictionary(); 31 | 32 | /// 33 | /// Gets a collection of cell style formats and their associated stylehseet position index. 34 | /// 35 | public Dictionary CellStyleFormats { get; } = new Dictionary(); 36 | 37 | /// 38 | /// Gets a collection of fills and their associated stylehseet position index. 39 | /// 40 | public Dictionary Fills { get; } = new Dictionary(); 41 | 42 | /// 43 | /// Gets a collection of fonts and their associated stylehseet position index. 44 | /// 45 | public Dictionary Fonts { get; } = new Dictionary(); 46 | 47 | /// 48 | /// Gets a collection of number formats and their associated stylehseet position index. 49 | /// 50 | public Dictionary NumberingFormats { get; } = new Dictionary(); 51 | 52 | /// 53 | /// Adds a border to the stylesheet. 54 | /// 55 | /// The border's cell placement. 56 | /// The border's style. 57 | /// The border's color. 58 | /// The stylesheet position index associated with the element. 59 | public uint AddBorder(BorderPlacement placement, OpenXml.BorderStyleValues style, in Color color) 60 | { 61 | var border = this.ConstructBorder(placement, style, color); 62 | return this.ResolveBorderKey(border); 63 | } 64 | 65 | /// 66 | /// Adds a border to the stylesheet. 67 | /// 68 | /// The OpenXml border element to be added. 69 | /// The stylesheet position index associated with the element. 70 | public uint AddBorder(OpenXml.Border border) 71 | { 72 | var clonedElement = (OpenXml.Border)border.CloneNode(true); 73 | return this.ResolveBorderKey(clonedElement); 74 | } 75 | 76 | /// 77 | /// Adds a cell format to the stylesheet. 78 | /// 79 | /// The stylesheet position index of the cell format's border property. 80 | /// The stylesheet position index of the cell format's fill property. 81 | /// The stylesheet position index of the cell format's font property. 82 | /// The stylesheet position index of the cell format's cell style format property. 83 | /// The stylesheet position index of the cell format's number format property. 84 | /// The cell format's horizontal alignment. 85 | /// The cell format's vertical alignment. 86 | /// The stylesheet position index associated with the element. 87 | public uint AddCellFormat( 88 | uint borderId = 0, 89 | uint fillId = 0, 90 | uint fontId = 0, 91 | uint cellStyleFormatId = 0, 92 | uint numberFormatId = (uint)OpenXmlNumberingFormat.General, 93 | OpenXml.HorizontalAlignmentValues horizontalAlignment = OpenXml.HorizontalAlignmentValues.General, 94 | OpenXml.VerticalAlignmentValues verticalAlignment = OpenXml.VerticalAlignmentValues.Center) 95 | { 96 | var cellFormat = this.ConstructCellFormat(borderId, fillId, fontId, cellStyleFormatId, numberFormatId, horizontalAlignment, verticalAlignment); 97 | return this.ResolveCellFormatKey(cellFormat); 98 | } 99 | 100 | /// 101 | /// Adds a cell format to the stylesheet. 102 | /// 103 | /// The OpenXml cell format element to be added. 104 | /// The stylesheet position index associated with the element. 105 | public uint AddCellFormat(OpenXml.CellFormat cellFormat) 106 | { 107 | var clonedElement = (OpenXml.CellFormat)cellFormat.CloneNode(true); 108 | return this.ResolveCellFormatKey(clonedElement); 109 | } 110 | 111 | /// 112 | /// Adds a cell style format to the stylesheet. 113 | /// 114 | /// The stylesheet position index of the cell style format's border property. 115 | /// The stylesheet position index of the cell format's fill property. 116 | /// The stylesheet position index of the cell format's font property. 117 | /// The stylesheet position index of the cell format's cell style format property. 118 | /// The stylesheet position index of the cell format's number format property. 119 | /// The cell format's horizontal alignment. 120 | /// The cell format's vertical alignment. 121 | /// The stylesheet position index associated with the element. 122 | public uint AddCellStyleFormat( 123 | uint borderId = 0, 124 | uint fillId = 0, 125 | uint fontId = 0, 126 | uint cellStyleFormatId = 0, 127 | uint numberFormatId = (uint)OpenXmlNumberingFormat.General, 128 | OpenXml.HorizontalAlignmentValues horizontalAlignment = OpenXml.HorizontalAlignmentValues.General, 129 | OpenXml.VerticalAlignmentValues verticalAlignment = OpenXml.VerticalAlignmentValues.Center) 130 | { 131 | var cellFormat = this.ConstructCellFormat(borderId, fillId, fontId, cellStyleFormatId, numberFormatId, horizontalAlignment, verticalAlignment); 132 | return this.ResolveCellStyleFormatKey(cellFormat); 133 | } 134 | 135 | /// 136 | /// Adds a cell style format to the stylesheet. 137 | /// 138 | /// The OpenXml cell style format element to be added. 139 | /// The stylesheet position index associated with the element. 140 | public uint AddCellStyleFormat(OpenXml.CellFormat cellStyleFormat) 141 | { 142 | var clonedElement = (OpenXml.CellFormat)cellStyleFormat.CloneNode(true); 143 | return this.ResolveCellStyleFormatKey(clonedElement); 144 | } 145 | 146 | /// 147 | /// Adds default styles to the styles collection. 148 | /// 149 | public void AddDefaultStyles() 150 | { 151 | this.AddBorder(BorderPlacement.None, OpenXml.BorderStyleValues.None, Color.Black); 152 | this.AddCellFormat(); 153 | this.AddCellStyleFormat(); 154 | this.AddPatternFill(Color.Transparent, OpenXml.PatternValues.None); 155 | this.AddPatternFill(Color.Transparent, OpenXml.PatternValues.Gray125); 156 | this.AddFont(new Font(FontFamily.GenericSansSerif, 11, FontStyle.Regular), Color.Black); 157 | } 158 | 159 | /// 160 | /// Adds a fill to the stylesheet. 161 | /// 162 | /// The OpenXml fill element to be added. 163 | /// The stylesheet position index associated with the element. 164 | public uint AddFill(OpenXml.Fill fill) 165 | { 166 | var clonedElement = (OpenXml.Fill)fill.CloneNode(true); 167 | return this.ResolveFillKey(clonedElement); 168 | } 169 | 170 | /// 171 | /// Adds a font to the styles collection. 172 | /// 173 | /// The font to be added. 174 | /// The font color to be applied. 175 | /// The stylesheet position index associated with the element. 176 | public uint AddFont(Font font, in Color fontColor) 177 | { 178 | var openXmlFont = new OpenXml.Font(); 179 | 180 | if (font.Bold) 181 | { 182 | openXmlFont.AppendChild(new OpenXml.Bold()); 183 | } 184 | 185 | if (font.Italic) 186 | { 187 | openXmlFont.AppendChild(new OpenXml.Italic()); 188 | } 189 | 190 | if (font.Strikeout) 191 | { 192 | openXmlFont.AppendChild(new OpenXml.Strike()); 193 | } 194 | 195 | if (font.Underline) 196 | { 197 | openXmlFont.AppendChild(new OpenXml.Underline()); 198 | } 199 | 200 | openXmlFont.AppendChild(new OpenXml.FontSize() { Val = font.Size }); 201 | 202 | if (fontColor == Color.Transparent) 203 | { 204 | throw new ArgumentException("Cannot convert Color.Transparent to a valid OpenXml font color."); 205 | } 206 | 207 | var colorText = ConvertColorToHex(fontColor); 208 | 209 | openXmlFont.AppendChild(ConvertHexColorToOpenXmlColor(colorText)); 210 | openXmlFont.AppendChild(new OpenXml.FontName() { Val = font.Name }); 211 | 212 | return this.ResolveFontKey(openXmlFont); 213 | } 214 | 215 | /// 216 | /// Adds a font to the stylesheet. 217 | /// 218 | /// The OpenXml font element to be added. 219 | /// The stylesheet position index associated with the element. 220 | public uint AddFont(OpenXml.Font font) 221 | { 222 | var clonedElement = (OpenXml.Font)font.CloneNode(true); 223 | return this.ResolveFontKey(clonedElement); 224 | } 225 | 226 | /// 227 | /// Adds a numbering format to the stylesheet. 228 | /// 229 | /// The number mask to be added. 230 | /// The stylesheet position index associated with the element. 231 | public uint AddNumberingFormat(string formatCode) 232 | { 233 | var numberingFormat = new OpenXml.NumberingFormat() { FormatCode = formatCode, }; 234 | return this.ResolveNumberingFormatKey(numberingFormat); 235 | } 236 | 237 | /// 238 | /// Adds a numbering format to the stylesheet. 239 | /// 240 | /// The OpenXml numbering format to be added. 241 | /// The stylesheet position index associated with the element. 242 | public uint AddNumberingFormat(OpenXml.NumberingFormat numberingFormat) 243 | { 244 | var clonedElement = (OpenXml.NumberingFormat)numberingFormat.CloneNode(true); 245 | return this.ResolveNumberingFormatKey(clonedElement); 246 | } 247 | 248 | /// 249 | /// Adds a pattern fill to the stylesheet. 250 | /// 251 | /// The fill's color. 252 | /// The fill's pattern type. 253 | /// The stylesheet position index associated with the element. 254 | public uint AddPatternFill(in Color color, OpenXml.PatternValues patternType = OpenXml.PatternValues.Solid) 255 | { 256 | var patternFill = new OpenXml.PatternFill() 257 | { 258 | PatternType = patternType, 259 | }; 260 | 261 | if (color != Color.Transparent) 262 | { 263 | var colorText = ConvertColorToHex(color); 264 | patternFill.ForegroundColor = new OpenXml.ForegroundColor { Rgb = new DocumentFormat.OpenXml.HexBinaryValue() { Value = colorText } }; 265 | } 266 | 267 | var fill = new OpenXml.Fill() { PatternFill = patternFill }; 268 | return this.ResolveFillKey(fill); 269 | } 270 | 271 | private static string ConvertColorToHex(in Color color) => Color.FromArgb(color.ToArgb()).Name; 272 | 273 | private static OpenXml.Color ConvertHexColorToOpenXmlColor(string hexColor) => new OpenXml.Color() { Rgb = new DocumentFormat.OpenXml.HexBinaryValue() { Value = hexColor } }; 274 | 275 | /// 276 | /// Creates an element. 277 | /// 278 | /// Appended XML nodes cannot use the same object instance, so that the Color object must be calculated for each border placement. 279 | private OpenXml.Border ConstructBorder(BorderPlacement placement, OpenXml.BorderStyleValues style, in Color color) 280 | { 281 | if (color == Color.Transparent) 282 | { 283 | throw new ArgumentException("Cannot convert Color.Transparent to a valid OpenXml border color."); 284 | } 285 | 286 | var colorText = ConvertColorToHex(color); 287 | var border = new OpenXml.Border(); 288 | 289 | // TODO: Replace bitwise operation with HasFlag when moved to .NET Core (performance issues fixed). 290 | if ((placement & BorderPlacement.Left) != 0) 291 | { 292 | border.AppendChild(new OpenXml.LeftBorder() { Color = ConvertHexColorToOpenXmlColor(colorText), Style = style }); 293 | } 294 | 295 | if ((placement & BorderPlacement.Right) != 0) 296 | { 297 | border.AppendChild(new OpenXml.RightBorder() { Color = ConvertHexColorToOpenXmlColor(colorText), Style = style }); 298 | } 299 | 300 | if ((placement & BorderPlacement.Top) != 0) 301 | { 302 | border.AppendChild(new OpenXml.TopBorder() { Color = ConvertHexColorToOpenXmlColor(colorText), Style = style }); 303 | } 304 | 305 | if ((placement & BorderPlacement.Bottom) != 0) 306 | { 307 | border.AppendChild(new OpenXml.BottomBorder() { Color = ConvertHexColorToOpenXmlColor(colorText), Style = style }); 308 | } 309 | 310 | if ((placement & BorderPlacement.DiagonalDown) != 0 || (placement & BorderPlacement.DiagonalUp) != 0) 311 | { 312 | border.AppendChild(new OpenXml.DiagonalBorder() { Color = ConvertHexColorToOpenXmlColor(colorText), Style = style }); 313 | border.DiagonalDown = (placement & BorderPlacement.DiagonalDown) != 0; 314 | border.DiagonalUp = (placement & BorderPlacement.DiagonalUp) != 0; 315 | } 316 | 317 | return border; 318 | } 319 | 320 | private OpenXml.CellFormat ConstructCellFormat( 321 | uint borderId = 0, 322 | uint fillId = 0, 323 | uint fontId = 0, 324 | uint cellFormatId = 0, 325 | uint numberFormatId = (uint)OpenXmlNumberingFormat.General, 326 | OpenXml.HorizontalAlignmentValues horizontalAlignment = OpenXml.HorizontalAlignmentValues.General, 327 | OpenXml.VerticalAlignmentValues verticalAlignment = OpenXml.VerticalAlignmentValues.Center) 328 | { 329 | bool applyFill = fillId != 0; 330 | bool applyNumberFormat = numberFormatId != 0; 331 | 332 | var cellFormat = new OpenXml.CellFormat() 333 | { 334 | ApplyFill = applyFill, 335 | ApplyNumberFormat = applyNumberFormat, 336 | BorderId = borderId, 337 | FillId = fillId, 338 | FontId = fontId, 339 | FormatId = cellFormatId, 340 | NumberFormatId = numberFormatId, 341 | }; 342 | 343 | cellFormat.AppendChild(new OpenXml.Alignment 344 | { 345 | Horizontal = horizontalAlignment, 346 | Vertical = verticalAlignment, 347 | }); 348 | 349 | return cellFormat; 350 | } 351 | 352 | private uint ResolveBorderKey(OpenXml.Border border) 353 | { 354 | var matchedItem = this.Borders.FirstOrDefault(x => x.Key.OuterXml == border.OuterXml); 355 | if (matchedItem.Key == null) 356 | { 357 | var value = (uint)this.Borders.Count; 358 | this.Borders.Add(border, value); 359 | return value; 360 | } 361 | 362 | return matchedItem.Value; 363 | } 364 | 365 | private uint ResolveCellFormatKey(OpenXml.CellFormat cellFormat) 366 | { 367 | var matchedItem = this.CellFormats.FirstOrDefault(x => x.Key.OuterXml == cellFormat.OuterXml); 368 | if (matchedItem.Key == null) 369 | { 370 | var value = (uint)this.CellFormats.Count; 371 | this.CellFormats.Add(cellFormat, value); 372 | return value; 373 | } 374 | 375 | return matchedItem.Value; 376 | } 377 | 378 | private uint ResolveCellStyleFormatKey(OpenXml.CellFormat cellStyleFormat) 379 | { 380 | var matchedItem = this.CellStyleFormats.FirstOrDefault(x => x.Key.OuterXml == cellStyleFormat.OuterXml); 381 | if (matchedItem.Key == null) 382 | { 383 | var value = (uint)this.CellStyleFormats.Count; 384 | this.CellStyleFormats.Add(cellStyleFormat, value); 385 | return value; 386 | } 387 | 388 | return matchedItem.Value; 389 | } 390 | 391 | private uint ResolveFillKey(OpenXml.Fill fill) 392 | { 393 | var matchedItem = this.Fills.FirstOrDefault(x => x.Key.OuterXml == fill.OuterXml); 394 | if (matchedItem.Key == null) 395 | { 396 | var value = (uint)this.Fills.Count; 397 | this.Fills.Add(fill, value); 398 | return value; 399 | } 400 | 401 | return matchedItem.Value; 402 | } 403 | 404 | private uint ResolveFontKey(OpenXml.Font font) 405 | { 406 | var matchedItem = this.Fonts.FirstOrDefault(x => x.Key.OuterXml == font.OuterXml); 407 | if (matchedItem.Key == null) 408 | { 409 | var value = (uint)this.Fonts.Count; 410 | this.Fonts.Add(font, value); 411 | return value; 412 | } 413 | 414 | return matchedItem.Value; 415 | } 416 | 417 | private uint ResolveNumberingFormatKey(OpenXml.NumberingFormat numberingFormat) 418 | { 419 | const uint minFormatId = 165; 420 | 421 | var matchedItem = this.NumberingFormats.FirstOrDefault(x => x.Key.FormatCode == numberingFormat.FormatCode); 422 | if (matchedItem.Key == null) 423 | { 424 | uint formatId = this.NumberingFormats.Count == 0 ? minFormatId : this.NumberingFormats.Values.Max() + 1; 425 | numberingFormat.NumberFormatId = formatId; 426 | this.NumberingFormats.Add(numberingFormat, formatId); 427 | 428 | this.NumberingFormats.TryGetValue(numberingFormat, out uint key); 429 | return key; 430 | } 431 | 432 | return matchedItem.Value; 433 | } 434 | } 435 | } -------------------------------------------------------------------------------- /src/OpenSpreadsheet/Configuration/WorksheetStyle.cs: -------------------------------------------------------------------------------- 1 | namespace OpenSpreadsheet.Configuration 2 | { 3 | using System.Drawing; 4 | 5 | using OpenXml = DocumentFormat.OpenXml.Spreadsheet; 6 | 7 | /// 8 | /// Encapsulates properties associated with a worksheet style definition. 9 | /// 10 | public class WorksheetStyle 11 | { 12 | /// 13 | /// Gets or sets the header background color. 14 | /// 15 | public virtual Color HeaderBackgroundColor { get; set; } = Color.Transparent; 16 | 17 | /// 18 | /// Gets or sets the header background pattern type. 19 | /// 20 | public virtual OpenXml.PatternValues HeaderBackgroundPatternType { get; set; } 21 | 22 | /// 23 | /// Gets or sets the header font. 24 | /// 25 | public virtual Font HeaderFont { get; set; } = new Font("Calibri", 11, FontStyle.Bold); 26 | 27 | /// 28 | /// Gets or sets the header foreground color. 29 | /// 30 | public virtual Color HeaderForegroundColor { get; set; } = Color.Black; 31 | 32 | /// 33 | /// Gets or sets the header horizontal alignment. 34 | /// 35 | public virtual OpenXml.HorizontalAlignmentValues HeaderHoizontalAlignment { get; set; } 36 | 37 | /// 38 | /// Gets or sets the header row index for writing. 39 | /// 40 | public virtual uint HeaderRowIndex { get; set; } = 1; 41 | 42 | /// 43 | /// Gets or set's the header vertical alignment. 44 | /// 45 | public virtual OpenXml.VerticalAlignmentValues HeaderVerticalAlignment { get; set; } = OpenXml.VerticalAlignmentValues.Center; 46 | 47 | /// 48 | /// Gets or sets the maximum column width. 49 | /// 50 | public virtual double MaxColumnWidth { get; set; } = 30.0; 51 | 52 | /// 53 | /// Gets or sets the minimum column width. 54 | /// 55 | public virtual double MinColumnWidth { get; set; } = 5.0; 56 | 57 | /// 58 | /// Gets or sets a value indicating whether the worksheet should automatically apply filters. 59 | /// 60 | public virtual bool ShouldAutoFilter { get; set; } 61 | 62 | /// 63 | /// Gets or sets a value indicating whether the worksheet should adjust column widths to their contents. 64 | /// 65 | /// Setting this value to true may have noticeable performance impacts for large data sets. 66 | public virtual bool ShouldAutoFitColumns { get; set; } 67 | 68 | /// 69 | /// Gets or sets a value indicating whether the header row should be frozen. 70 | /// 71 | public virtual bool ShouldFreezeHeaderRow { get; set; } 72 | 73 | /// 74 | /// Gets or sets a value indicating whether the header row should be written. 75 | /// 76 | public virtual bool ShouldWriteHeaderRow { get; set; } = true; 77 | } 78 | } -------------------------------------------------------------------------------- /src/OpenSpreadsheet/Enums/BorderPlacement.cs: -------------------------------------------------------------------------------- 1 | namespace OpenSpreadsheet.Enums 2 | { 3 | using System; 4 | 5 | /// 6 | /// Encapsulates valid cell border placement values. 7 | /// 8 | [Flags] 9 | public enum BorderPlacement : uint 10 | { 11 | /// 12 | /// No borders. 13 | /// 14 | None = 0, 15 | 16 | /// 17 | /// Border on the left side of the cell. 18 | /// 19 | Left = 1 << 0, 20 | 21 | /// 22 | /// Border on the right side of the cell. 23 | /// 24 | Right = 1 << 1, 25 | 26 | /// 27 | /// Border on the top side of the cell. 28 | /// 29 | Top = 1 << 2, 30 | 31 | /// 32 | /// Border on the bottom side of the cell. 33 | /// 34 | Bottom = 1 << 3, 35 | 36 | /// 37 | /// Border on the entire outside of the cell. 38 | /// 39 | Outside = Left | Right | Top | Bottom, 40 | 41 | /// 42 | /// Border from the bottom left to the top right of the cell. 43 | /// 44 | DiagonalUp = 1 << 4, 45 | 46 | /// 47 | /// Border from the bottom right to the top left of the cell. 48 | /// 49 | DiagonalDown = 1 << 5, 50 | 51 | /// 52 | /// All possible borders. 53 | /// 54 | All = Outside | DiagonalUp | DiagonalDown 55 | } 56 | } -------------------------------------------------------------------------------- /src/OpenSpreadsheet/Enums/ColumnType.cs: -------------------------------------------------------------------------------- 1 | namespace OpenSpreadsheet.Enums 2 | { 3 | /// 4 | /// Encapsulates valid spreadsheet column types. 5 | /// 6 | public enum ColumnType 7 | { 8 | /// 9 | /// No explicit column type defined. 10 | /// 11 | Unset, 12 | 13 | /// 14 | /// A column containing boolean values. 15 | /// 16 | Boolean, 17 | 18 | /// 19 | /// A column containing datetime values. 20 | /// 21 | Date, 22 | 23 | /// 24 | /// A column containing formulas. 25 | /// 26 | Formula, 27 | 28 | /// 29 | /// A column containing numeric values. 30 | /// 31 | Number, 32 | 33 | /// 34 | /// A column containing rich text values. 35 | /// 36 | RichText, 37 | 38 | /// 39 | /// A column containing text values. 40 | /// 41 | Text, 42 | } 43 | } -------------------------------------------------------------------------------- /src/OpenSpreadsheet/Enums/OpenXmlNumberingFormat.cs: -------------------------------------------------------------------------------- 1 | namespace OpenSpreadsheet.Enums 2 | { 3 | /// 4 | /// Encapsulates valid builtin cell numbering formats. 5 | /// 6 | public enum OpenXmlNumberingFormat : uint 7 | { 8 | /// 9 | /// A general format that will make a best guess at displaying data. 10 | /// 11 | General = 0, 12 | 13 | /// 14 | /// A general format for numeric data (mask "0"). 15 | /// 16 | Number = 1, 17 | 18 | /// 19 | /// A general format for decimal data (mask "0.00"). 20 | /// 21 | Decimal = 2, 22 | 23 | /// 24 | /// A numeric format that uses comma separators (mask "#,##0"). 25 | /// 26 | NumberWithCommas = 3, 27 | 28 | /// 29 | /// A numeric format that rounds decimal points to two places (mask "#,##0.00"). 30 | /// 31 | Currency = 4, 32 | 33 | /// 34 | /// A numeric format that multiplies the value by 100 and displays the result with a percentage sign, truncating any decimal amounts (mask "0%"). 35 | /// 36 | Percentage = 9, 37 | 38 | /// 39 | /// A numeric format that multiplies the value by 100 and displays the result with a percentage sign, displaying any decimal amounts (mask "0.00%"). 40 | /// 41 | PercentageDecimal = 10, 42 | 43 | /// 44 | /// A numeric format that uses exponential notation (mask "0.00E+00"). 45 | /// 46 | Scientific = 11, 47 | 48 | /// 49 | /// A numeric format that rounds to the nearest single-digit fraction value (mask "# ?/?"). 50 | /// 51 | FractionOneDigit = 12, 52 | 53 | /// 54 | /// A numeric format that rounds to the nearest two-digit fraction value (mask "# ??/??"). 55 | /// 56 | FractionTwoDigits = 13, 57 | 58 | /// 59 | /// A datetime format that displays a date in mm-dd-yy format (e.g., 01-09-19). 60 | /// 61 | /// Excel implements this numbering format as m/dd/yyyy rather than the format defined in the standard. 62 | DateMonthDayYear = 14, 63 | 64 | /// 65 | /// A datetime format that displays a date in d-mmm-yy format (e.g., 9-Jan-19). 66 | /// 67 | DateDayMonthYear = 15, 68 | 69 | /// 70 | /// A datetime format that displays a date in d-mmm format (e.g., 9-Jan). 71 | /// 72 | DateDayMonth = 16, 73 | 74 | /// 75 | /// A datetime format that displays a date in mmm-yy format (e.g., Jan-19). 76 | /// 77 | DateMonthYear = 17, 78 | 79 | /// 80 | /// A datetime format that displays a time in h:m AM/PM format, using a twelve-hour clock (e.g., 5:54 PM). 81 | /// 82 | TimestampHourMinute12 = 18, 83 | 84 | /// 85 | /// A datetime format that displays a time in h:m:s AM/PM format, using a twelve-hour clock (e.g., 5:54:35 PM). 86 | /// 87 | TimestampHouMinuteSecond12 = 19, 88 | 89 | /// 90 | /// A datetime format that displays a time in h:m format, using a twenty-four-hour clock (e.g., 17:54). 91 | /// 92 | TimestampHourMinute24 = 20, 93 | 94 | /// 95 | /// A datetime format that displays a time in h:m:s format, using a twenty-four-hour clock (e.g., 17:54:35). 96 | /// 97 | TimestampHouMinuteSecond24 = 21, 98 | 99 | /// 100 | /// A datetime format that displays a datetime in m/d/yyyy h:m format (e.g., 1/9/2019 17:54). 101 | /// 102 | DatestampWithTime = 22, 103 | 104 | /// 105 | /// A numeric format that displays negative numbers surrounded by parentheses (mask "#,##0 ;(#,##0)"). 106 | /// 107 | NumberWithNegativeInParens = 37, 108 | 109 | /// 110 | /// A numeric format that displays negative numbers in red text, surrounded by parentheses (mask "#,##0 ;[Red](#,##0)"). 111 | /// 112 | NumberWithNegativeInRedParens = 38, 113 | 114 | /// 115 | /// A numeric format that displays negative numbers surrounded by parentheses (mask "#,##0.00;(#,##0.00)"). 116 | /// 117 | DecimalWithNegativeInParens = 39, 118 | 119 | /// 120 | /// A decimal format that displays negative numbers in red text, surrounded by parentheses (mask "#,##0.00;[Red](#,##0.00)"). 121 | /// 122 | DecimalWithNegativeInRedParens = 40, 123 | 124 | /// 125 | /// A decimal format that displays currency amounts (mask "'_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)';"). 126 | /// 127 | /// This numbering format is not part of the OpenXml standard and may not be supported by all applications. 128 | Accounting = 44, 129 | 130 | /// 131 | /// A timestamp format that displays a time in m:ss format (e.g., 1:15). 132 | /// 133 | TimestampMinuteSecond = 45, 134 | 135 | /// 136 | /// A timestamp format that displays a time in [h]:m:ss format, displaying the hour position only if applicable (e.g., 7:1:15). 137 | /// 138 | TimestampConditionalHourMinuteSecond = 46, 139 | 140 | /// 141 | /// A format for text. 142 | /// 143 | Text = 49, 144 | } 145 | } -------------------------------------------------------------------------------- /src/OpenSpreadsheet/Extensions/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace OpenSpreadsheet.Extensions 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | public static class TypeExtensions 9 | { 10 | private static readonly HashSet numericTypes = new HashSet() 11 | { 12 | typeof(byte), typeof(decimal), typeof(double), typeof(float), typeof(int), typeof(long), typeof(sbyte), typeof(short), typeof(uint), typeof(ulong), typeof(ushort), 13 | }; 14 | 15 | public static bool IsNumeric(this Type type) => numericTypes.Contains(type); 16 | } 17 | } -------------------------------------------------------------------------------- /src/OpenSpreadsheet/OpenSpreadsheet.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0;net45 5 | 7.2 6 | true 7 | true 8 | 1.2.3.0 9 | Steve Stanzak 10 | OpenSpreadsheet 11 | A fast and efficient wrapper around the OpenXml Excel library 12 | OpenSpreadsheet 13 | MIT 14 | https://github.com/FolkCoder/OpenSpreadsheet 15 | https://github.com/FolkCoder/OpenSpreadsheet 16 | excel, openxml, spreadsheets, productivity, xlsx, openspreadsheet 17 | © 2019 Steve Stanzak 18 | 19 | 1.2.3.0 20 | 1.2.3.0 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 4.5.1 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/OpenSpreadsheet/ReaderRow.cs: -------------------------------------------------------------------------------- 1 | namespace OpenSpreadsheet 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | /// 8 | /// Encapsulates properties associated with a spreadsheet row. 9 | /// 10 | public class ReaderRow 11 | { 12 | private readonly BidirectionalDictionary headers; 13 | private readonly Dictionary rowValues; 14 | 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// A dictionary containing header column indexes and their associated names. 19 | /// A dictionary containing row column indeces and their associated values. 20 | public ReaderRow(BidirectionalDictionary headers, Dictionary rowValues) 21 | { 22 | this.headers = headers; 23 | this.rowValues = rowValues; 24 | } 25 | 26 | /// 27 | /// Retrieves the value of the cell at the provided column. 28 | /// 29 | /// The column header name. 30 | /// The cell's value. 31 | public string GetCellValue(string headerName) 32 | { 33 | this.headers.TryGetKey(headerName, out uint columnIndex); 34 | if (columnIndex == 0) 35 | { 36 | throw new KeyNotFoundException(); 37 | } 38 | 39 | this.rowValues.TryGetValue(columnIndex, out string value); 40 | if (value == null) 41 | { 42 | throw new KeyNotFoundException(); 43 | } 44 | 45 | return value; 46 | } 47 | 48 | /// 49 | /// Retrieves the value of the cell at the specified column index. 50 | /// 51 | /// The one-based column index where the cell is located. 52 | /// The cell's value. 53 | public string GetCellValue(uint columnIndex) 54 | { 55 | this.rowValues.TryGetValue(columnIndex, out string value); 56 | if (value == null) 57 | { 58 | throw new KeyNotFoundException(); 59 | } 60 | 61 | return value; 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/OpenSpreadsheet/WorksheetReader.cs: -------------------------------------------------------------------------------- 1 | namespace OpenSpreadsheet 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Globalization; 6 | using System.Linq; 7 | 8 | using DocumentFormat.OpenXml; 9 | using DocumentFormat.OpenXml.Packaging; 10 | using DocumentFormat.OpenXml.Spreadsheet; 11 | 12 | using OpenSpreadsheet.Configuration; 13 | using OpenSpreadsheet.Extensions; 14 | 15 | /// 16 | /// Writes data to a worksheet. 17 | /// 18 | /// The class type to be written. 19 | /// A map defining how to write record data to the worksheet. 20 | public class WorksheetReader : IDisposable 21 | where TClass : class, new() 22 | where TClassMap : ClassMap 23 | { 24 | private const string rowIndexAttribute = "r"; 25 | 26 | private readonly Dictionary columnCellReferences = new Dictionary(); 27 | private readonly IReadOnlyCollection> propertyMaps; 28 | private readonly OpenXmlReader reader; 29 | private readonly BidirectionalDictionary sharedStrings; 30 | private readonly SpreadsheetDocument spreadsheetDocument; 31 | 32 | private uint currentRowIndex = 1; 33 | private bool isDisposed; 34 | 35 | /// 36 | /// Initializes a new instance of the class. 37 | /// 38 | /// 39 | /// 40 | /// 41 | /// 42 | public WorksheetReader(string worksheetName, SpreadsheetDocument spreadsheetDocument, BidirectionalDictionary sharedStrings, uint headerRowIndex = 1) 43 | { 44 | this.sharedStrings = sharedStrings; 45 | this.spreadsheetDocument = spreadsheetDocument; 46 | 47 | // reader setup 48 | var worksheetId = this.spreadsheetDocument.WorkbookPart.Workbook.Descendants().First(s => worksheetName.Equals(s.Name)).Id; 49 | var worksheetPart = this.spreadsheetDocument.WorkbookPart.GetPartById(worksheetId); 50 | this.reader = OpenXmlReader.Create(worksheetPart); 51 | this.reader.Read(); 52 | this.ReadHeader(headerRowIndex); 53 | 54 | // map setup 55 | var classMap = Activator.CreateInstance(); 56 | this.propertyMaps = classMap.PropertyMaps.Where(x => !x.PropertyData.IgnoreRead).ToList().AsReadOnly(); 57 | } 58 | 59 | /// 60 | /// Gets a collection of key value pairs containing a header column index and its name. 61 | /// 62 | public BidirectionalDictionary Headers { get; } = new BidirectionalDictionary(); 63 | 64 | /// 65 | /// Read a single row at the current position and map its data to an object. 66 | /// 67 | /// A mapped object. 68 | public TClass ReadRow() 69 | { 70 | var readerRow = this.ReadRowValues(); 71 | if (readerRow == null) 72 | { 73 | return null; 74 | } 75 | 76 | var record = new TClass(); 77 | foreach (var map in this.propertyMaps) 78 | { 79 | var propertyInfo = record.GetType().GetProperty(map.PropertyData.Property.Name); 80 | if (!propertyInfo.CanWrite) 81 | { 82 | continue; 83 | } 84 | 85 | if (map.PropertyData.ConstantRead != null) 86 | { 87 | propertyInfo.SetValue(record, map.PropertyData.ConstantRead); 88 | continue; 89 | } 90 | 91 | if (map.PropertyData.ReadUsing != null) 92 | { 93 | var expressionValue = map.PropertyData.ReadUsing(readerRow); 94 | propertyInfo.SetValue(record, expressionValue); 95 | continue; 96 | } 97 | 98 | uint columnIndex; 99 | if (map.PropertyData.IndexRead > 0) 100 | { 101 | columnIndex = map.PropertyData.IndexRead; 102 | } 103 | else 104 | { 105 | var cellName = string.IsNullOrWhiteSpace(map.PropertyData.NameRead) 106 | ? map.PropertyData.Property.Name 107 | : map.PropertyData.NameRead; 108 | 109 | if (!this.Headers.TryGetKey(cellName, out columnIndex)) 110 | { 111 | throw new InvalidOperationException($"The ClassMap contains invalid read maps. The property {map.PropertyData.Property.Name} has no index defined and there is no spreadsheet column matching either the property name or a defined name property."); 112 | } 113 | } 114 | 115 | var cellValue = readerRow.GetCellValue(columnIndex); 116 | 117 | if (map.PropertyData.DefaultRead != null && cellValue.Length == 0) 118 | { 119 | propertyInfo.SetValue(record, map.PropertyData.DefaultRead); 120 | continue; 121 | } 122 | 123 | var propertyType = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType; 124 | 125 | object safeValue; 126 | if (propertyType == typeof(bool)) 127 | { 128 | safeValue = (cellValue == null) ? null : (object)ConvertStringToBool(cellValue); 129 | } 130 | else if (propertyType == typeof(DateTime)) 131 | { 132 | safeValue = (cellValue == null) ? null : (object)ConvertDateTime(cellValue); 133 | } 134 | else if (propertyType.IsEnum) 135 | { 136 | safeValue = (cellValue == null) ? null : Enum.Parse(propertyType, cellValue); 137 | } 138 | else if (propertyType.IsNumeric()) 139 | { 140 | if (double.TryParse(cellValue, NumberStyles.AllowExponent | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out double doubleValue)) 141 | { 142 | safeValue = (cellValue == null) ? null : Convert.ChangeType(doubleValue, propertyType); 143 | } 144 | else 145 | { 146 | safeValue = (cellValue == null) ? null : Convert.ChangeType(cellValue, propertyType); 147 | } 148 | } 149 | else 150 | { 151 | safeValue = (cellValue == null) ? null : Convert.ChangeType(cellValue, propertyType); 152 | } 153 | 154 | propertyInfo.SetValue(record, safeValue, null); 155 | } 156 | 157 | return record; 158 | } 159 | 160 | /// 161 | /// Read all rows starting from the current position and map the data to a collection of objects. 162 | /// 163 | /// A collection of mapped objects. 164 | public IEnumerable ReadRows() 165 | { 166 | var records = new List(); 167 | do 168 | { 169 | var record = this.ReadRow(); 170 | if (record != null) 171 | { 172 | records.Add(record); 173 | } 174 | } while (!this.reader.EOF); 175 | 176 | return records; 177 | } 178 | 179 | /// 180 | /// Skip a single row. 181 | /// 182 | public void SkipRow() => this.SkipRows(1); 183 | 184 | /// 185 | /// Skip one or more rows. 186 | /// 187 | /// The number of rows to skip. 188 | public void SkipRows(uint count) 189 | { 190 | var targetRow = this.currentRowIndex + count; 191 | do 192 | { 193 | this.AdvanceToRowStart(); 194 | if (this.currentRowIndex < targetRow) 195 | { 196 | this.reader.Skip(); 197 | } 198 | else 199 | { 200 | return; 201 | } 202 | } while (!this.reader.EOF); 203 | } 204 | 205 | private void AdvanceToRowStart() 206 | { 207 | while (!this.reader.EOF) 208 | { 209 | if (this.reader.ElementType == typeof(Row) && this.reader.IsStartElement) 210 | { 211 | this.currentRowIndex = uint.Parse(this.reader.Attributes.First(r => r.LocalName == rowIndexAttribute).Value); 212 | return; 213 | } 214 | else 215 | { 216 | this.reader.Read(); 217 | } 218 | } 219 | } 220 | 221 | private static DateTime ConvertDateTime(string date) 222 | { 223 | if (DateTime.TryParse(date, out DateTime datetimeResult)) 224 | { 225 | return datetimeResult; 226 | } 227 | 228 | if (double.TryParse(date, out double doubleResult)) 229 | { 230 | return DateTime.FromOADate(doubleResult); 231 | } 232 | 233 | throw new InvalidCastException(); 234 | } 235 | 236 | private static bool ConvertStringToBool(string textBool) 237 | { 238 | if (bool.TryParse(textBool, out bool boolValue)) 239 | { 240 | return boolValue; 241 | } 242 | 243 | if (int.TryParse(textBool, out int intValue)) 244 | { 245 | return (bool)Convert.ChangeType(intValue, typeof(bool)); 246 | } 247 | 248 | throw new InvalidCastException(); 249 | } 250 | 251 | private static string GetCellValue(BidirectionalDictionary sharedStrings, Cell cell) 252 | { 253 | if (cell.CellValue == null) 254 | { 255 | return string.Empty; 256 | } 257 | 258 | if (cell.DataType == null || cell.DataType != CellValues.SharedString) 259 | { 260 | return cell.CellValue.InnerText; 261 | } 262 | 263 | sharedStrings.TryGetKey(cell.CellValue.InnerText, out string sharedStringValue); 264 | return sharedStringValue; 265 | } 266 | 267 | /// 268 | /// Determines a cell's one-based column index from its Excel cell position (e.g., A1). 269 | /// 270 | /// The cell reference to be converted. 271 | /// The cell's numeric column index. 272 | private uint GetColumnIndexFromCellReference(string cellReference) 273 | { 274 | var columnLetters = cellReference.Where(c => !char.IsNumber(c)).ToArray(); 275 | string columnReference = new string(columnLetters); 276 | if (this.columnCellReferences.ContainsKey(columnReference)) 277 | { 278 | return this.columnCellReferences[columnReference]; 279 | } 280 | 281 | int sum = 0; 282 | for (int i = 0; i < columnLetters.Length; i++) 283 | { 284 | sum *= 26; 285 | sum += columnLetters[i] - 'A' + 1; 286 | } 287 | 288 | this.columnCellReferences.Add(columnReference, (uint)sum); 289 | return (uint)sum; 290 | } 291 | 292 | private void ReadHeader(uint headerRowIndex) 293 | { 294 | if (headerRowIndex == 0) 295 | { 296 | return; 297 | } 298 | 299 | this.SkipRows(headerRowIndex - 1); 300 | 301 | this.AdvanceToRowStart(); 302 | if (this.reader.EOF) 303 | { 304 | throw new InvalidOperationException("There are no rows available to read."); 305 | } 306 | 307 | this.reader.ReadFirstChild(); 308 | do 309 | { 310 | if (this.reader.ElementType == typeof(Cell)) 311 | { 312 | var cell = (Cell)this.reader.LoadCurrentElement(); 313 | var cellValue = GetCellValue(this.sharedStrings, cell); 314 | var columnIndex = this.GetColumnIndexFromCellReference(cell.CellReference); 315 | 316 | this.Headers.Add(columnIndex, cellValue); 317 | } 318 | } while (this.reader.ReadNextSibling()); 319 | } 320 | 321 | private ReaderRow ReadRowValues() 322 | { 323 | this.AdvanceToRowStart(); 324 | if (this.reader.EOF) 325 | { 326 | return null; 327 | } 328 | 329 | this.reader.ReadFirstChild(); 330 | var rowValues = new Dictionary(); 331 | 332 | do 333 | { 334 | if (this.reader.ElementType == typeof(Cell)) 335 | { 336 | var cell = (Cell)this.reader.LoadCurrentElement(); 337 | var cellValue = GetCellValue(this.sharedStrings, cell); 338 | var columnIndex = this.GetColumnIndexFromCellReference(cell.CellReference); 339 | 340 | rowValues.Add(columnIndex, cellValue); 341 | } 342 | } while (this.reader.ReadNextSibling()); 343 | 344 | return new ReaderRow(this.Headers, rowValues); 345 | } 346 | 347 | #region IDisposable 348 | 349 | /// 350 | /// Closes the object and the underlying stream, and releases any the system resources associated with the reader. 351 | /// 352 | public void Close() 353 | { 354 | this.Dispose(true); 355 | GC.SuppressFinalize(this); 356 | } 357 | 358 | /// 359 | /// Closes the object and the underlying stream, and releases any the system resources associated with the reader. 360 | /// 361 | public void Dispose() 362 | { 363 | this.Dispose(true); 364 | GC.SuppressFinalize(this); 365 | } 366 | 367 | /// 368 | /// Closes the object and the underlying stream, and optionally releases any the system resources associated with the reader. 369 | /// 370 | protected virtual void Dispose(bool disposing) 371 | { 372 | if (!this.isDisposed && disposing) 373 | { 374 | this.reader.Close(); 375 | } 376 | 377 | this.isDisposed = true; 378 | } 379 | 380 | #endregion IDisposable 381 | } 382 | } -------------------------------------------------------------------------------- /src/Tests/Configuration/ClassMaps.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Configuration 2 | { 3 | using OpenSpreadsheet.Configuration; 4 | using Xunit; 5 | 6 | public class ClassMaps 7 | { 8 | [Fact] 9 | public void TestConstantsValidation() 10 | { 11 | var validator = new ConfigurationValidator(); 12 | validator.Validate(); 13 | Assert.True(validator.Errors.Count == 1); 14 | } 15 | 16 | [Fact] 17 | public void TestDefaultsValidation() 18 | { 19 | var validator = new ConfigurationValidator(); 20 | validator.Validate(); 21 | Assert.True(validator.Errors.Count == 1); 22 | } 23 | 24 | [Fact] 25 | public void TestDuplicateIndexesValidation() 26 | { 27 | var classMap = new TestClassMapDuplicateIndexes(); 28 | var validator = new ConfigurationValidator(); 29 | validator.Validate(); 30 | Assert.True(validator.HasErrors); 31 | } 32 | 33 | [Fact] 34 | public void TestDuplicateReadPropertiesValidation() 35 | { 36 | var classMap = new TestClassMapDuplicateReadProperties(); 37 | var validator = new ConfigurationValidator(); 38 | validator.Validate(); 39 | Assert.True(validator.HasErrors); 40 | } 41 | 42 | [Fact] 43 | public void TestIndexOutOfRangeValidation() 44 | { 45 | var classMap = new TestClassMapIndexOutOfRange(); 46 | var validator = new ConfigurationValidator(); 47 | validator.Validate(); 48 | Assert.True(validator.Errors.Count == 2); 49 | } 50 | 51 | [Fact] 52 | public void TestLongHeadersValidation() 53 | { 54 | var classMap = new TestClassMapLongHeaders(); 55 | var validator = new ConfigurationValidator(); 56 | validator.Validate(); 57 | Assert.True(validator.Errors.Count == 1); 58 | } 59 | 60 | private class TestClass 61 | { 62 | public string TestData { get; set; } = "test data"; 63 | public string TestDataNull { get; set; } = null; 64 | } 65 | 66 | private class TestClassMapConstants : ClassMap 67 | { 68 | public TestClassMapConstants() 69 | { 70 | base.Map(x => x.TestData).Index(1).Constant(2312.231M); 71 | base.Map(x => x.TestDataNull).Index(2); 72 | } 73 | } 74 | 75 | private class TestClassMapDefaults : ClassMap 76 | { 77 | public TestClassMapDefaults() 78 | { 79 | base.Map(x => x.TestDataNull).Index(1).Default(2312.231M); 80 | base.Map(x => x.TestData).Index(2); 81 | } 82 | } 83 | 84 | private class TestClassMapDuplicateIndexes : ClassMap 85 | { 86 | public TestClassMapDuplicateIndexes() 87 | { 88 | base.Map(x => x.TestData).Index(1); 89 | base.Map(x => x.TestDataNull).Index(1); 90 | } 91 | } 92 | 93 | private class TestClassMapDuplicateReadProperties : ClassMap 94 | { 95 | public TestClassMapDuplicateReadProperties() 96 | { 97 | base.Map(x => x.TestData).Index(1); 98 | base.Map(x => x.TestData).Index(2); 99 | } 100 | } 101 | 102 | private class TestClassMapIndexOutOfRange : ClassMap 103 | { 104 | public TestClassMapIndexOutOfRange() 105 | { 106 | base.Map(x => x.TestData).Index(1); 107 | base.Map(x => x.TestDataNull).Index(16385); 108 | } 109 | } 110 | 111 | private class TestClassMapLongHeaders : ClassMap 112 | { 113 | public TestClassMapLongHeaders() 114 | { 115 | string longHeader = new string('a', 256); 116 | base.Map(x => x.TestData).Index(1).Name(longHeader); 117 | } 118 | } 119 | 120 | private class TestClassMapMissingIndexes : ClassMap 121 | { 122 | public TestClassMapMissingIndexes() 123 | { 124 | base.Map(x => x.TestData).Index(1); 125 | base.Map(x => x.TestDataNull); 126 | } 127 | } 128 | } 129 | } -------------------------------------------------------------------------------- /src/Tests/Configuration/ImpliedMappings.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Configuration 2 | { 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using DocumentFormat.OpenXml.Packaging; 7 | using DocumentFormat.OpenXml.Spreadsheet; 8 | using OpenSpreadsheet; 9 | using OpenSpreadsheet.Configuration; 10 | using Xunit; 11 | 12 | public class ImpliedMappings : SpreadsheetTesterBase 13 | { 14 | [Fact] 15 | public void TestReadByHeaderName() 16 | { 17 | var filepath = base.ConstructTempXlsxSaveName(); 18 | using (var spreadsheet = new Spreadsheet(filepath)) 19 | { 20 | spreadsheet.WriteWorksheet("Sheet1", base.CreateRecords(10)); 21 | } 22 | 23 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 24 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 25 | { 26 | base.SpreadsheetValidator.Validate(spreadsheetFile); 27 | Assert.False(base.SpreadsheetValidator.HasErrors); 28 | 29 | using (var spreadsheet = new Spreadsheet(spreadsheetFile)) 30 | { 31 | foreach (var record in spreadsheet.ReadWorksheet("Sheet1")) 32 | { 33 | Assert.Equal(1, record.TestData1); 34 | Assert.Equal(2, record.TestData2); 35 | Assert.Equal(3, record.TestData3); 36 | Assert.Equal(4, record.TestData4); 37 | Assert.Equal(5, record.TestData5); 38 | } 39 | } 40 | 41 | File.Delete(spreadsheetFile); 42 | } 43 | } 44 | 45 | [Fact] 46 | public void TestWriteByMappingOrder() 47 | { 48 | var writeOrder = new Dictionary() 49 | { 50 | { 1, 1 }, 51 | { 2, 5 }, 52 | { 3, 4 }, 53 | { 4, 3 }, 54 | { 5, 2 }, 55 | }; 56 | 57 | var filepath = base.ConstructTempXlsxSaveName(); 58 | using (var spreadsheet = new Spreadsheet(filepath)) 59 | { 60 | spreadsheet.WriteWorksheet("Sheet1", base.CreateRecords(10), new WorksheetStyle() { ShouldWriteHeaderRow = false} ); 61 | } 62 | 63 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 64 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 65 | { 66 | base.SpreadsheetValidator.Validate(spreadsheetFile); 67 | Assert.False(base.SpreadsheetValidator.HasErrors); 68 | 69 | using (var filestream = new FileStream(spreadsheetFile, FileMode.Open, FileAccess.Read, FileShare.Read)) 70 | using (var spreadsheetDocument = SpreadsheetDocument.Open(filestream, false)) 71 | { 72 | var workbookPart = spreadsheetDocument.WorkbookPart; 73 | var worksheetPart = workbookPart.WorksheetParts.First(); 74 | var sheet = worksheetPart.Worksheet; 75 | 76 | foreach (var cell in sheet.Descendants()) 77 | { 78 | var columnIndex = base.GetColumnIndexFromCellReference(cell.CellReference); 79 | var expectedValue = writeOrder[columnIndex]; 80 | 81 | Assert.Equal(expectedValue, int.Parse(cell.InnerText)); 82 | } 83 | } 84 | 85 | File.Delete(spreadsheetFile); 86 | } 87 | } 88 | 89 | private class TestClass 90 | { 91 | public int TestData1 { get; set; } = 1; 92 | public int TestData2 { get; set; } = 2; 93 | public int TestData3 { get; set; } = 3; 94 | public int TestData4 { get; set; } = 4; 95 | public int TestData5 { get; set; } = 5; 96 | } 97 | 98 | private class TestClassMap : ClassMap 99 | { 100 | public TestClassMap() 101 | { 102 | base.Map(x => x.TestData1).Index(5); 103 | base.Map(x => x.TestData2).Index(4); 104 | base.Map(x => x.TestData3).Index(3); 105 | base.Map(x => x.TestData4).Index(2); 106 | base.Map(x => x.TestData5).Index(1); 107 | } 108 | } 109 | 110 | private class TestClassMapReadByHeaderName : ClassMap 111 | { 112 | public TestClassMapReadByHeaderName() 113 | { 114 | base.Map(x => x.TestData5); 115 | base.Map(x => x.TestData4); 116 | base.Map(x => x.TestData3); 117 | base.Map(x => x.TestData2); 118 | base.Map(x => x.TestData1); 119 | } 120 | } 121 | 122 | private class TestClassMapWriteByOrder : ClassMap 123 | { 124 | public TestClassMapWriteByOrder() 125 | { 126 | base.Map(x => x.TestData5); 127 | base.Map(x => x.TestData4); 128 | base.Map(x => x.TestData3); 129 | base.Map(x => x.TestData2); 130 | base.Map(x => x.TestData1).Index(1); 131 | } 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /src/Tests/DataConversions/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.DataConversions 2 | { 3 | using System; 4 | using System.IO; 5 | using OpenSpreadsheet; 6 | using OpenSpreadsheet.Configuration; 7 | 8 | using Xunit; 9 | 10 | public class Constants : SpreadsheetTesterBase 11 | { 12 | private static readonly TestClass testClassWithConstantValues = new TestClass() 13 | { 14 | Bool = true, 15 | Byte = 24, 16 | Char = 'h', 17 | DateTime = new DateTime(2019, 10, 31), 18 | Decimal = 433123.12M, 19 | Double = 6.1E+3, 20 | Float = 23342.93F, 21 | Int = 551412, 22 | Long = 4324823423423, 23 | Text = "constant string", 24 | }; 25 | 26 | [Fact] 27 | public void TestRead() 28 | { 29 | var filepath = base.ConstructTempXlsxSaveName(); 30 | using (var spreadsheet = new Spreadsheet(filepath)) 31 | { 32 | spreadsheet.WriteWorksheet("Sheet1", base.CreateRecords(10)); 33 | } 34 | 35 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 36 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 37 | { 38 | base.SpreadsheetValidator.Validate(spreadsheetFile); 39 | Assert.False(base.SpreadsheetValidator.HasErrors); 40 | 41 | using (var spreadsheet = new Spreadsheet(spreadsheetFile)) 42 | { 43 | foreach (var record in spreadsheet.ReadWorksheet("Sheet1")) 44 | { 45 | Assert.Equal(record.Bool, testClassWithConstantValues.Bool); 46 | Assert.Equal(record.Byte, testClassWithConstantValues.Byte); 47 | Assert.Equal(record.Char, testClassWithConstantValues.Char); 48 | Assert.Equal(record.DateTime, testClassWithConstantValues.DateTime); 49 | Assert.Equal(record.Decimal, testClassWithConstantValues.Decimal); 50 | Assert.Equal(record.Double, testClassWithConstantValues.Double); 51 | Assert.Equal(record.Float, testClassWithConstantValues.Float); 52 | Assert.Equal(record.Int, testClassWithConstantValues.Int); 53 | Assert.Equal(record.Long, testClassWithConstantValues.Long); 54 | Assert.Equal(record.Text, testClassWithConstantValues.Text); 55 | } 56 | } 57 | 58 | File.Delete(spreadsheetFile); 59 | } 60 | } 61 | 62 | [Fact] 63 | public void TestWrite() 64 | { 65 | var filepath = base.ConstructTempXlsxSaveName(); 66 | using (var spreadsheet = new Spreadsheet(filepath)) 67 | { 68 | spreadsheet.WriteWorksheet("Sheet1", base.CreateRecords(10)); 69 | } 70 | 71 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 72 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 73 | { 74 | base.SpreadsheetValidator.Validate(spreadsheetFile); 75 | Assert.False(base.SpreadsheetValidator.HasErrors); 76 | 77 | using (var spreadsheet = new Spreadsheet(spreadsheetFile)) 78 | { 79 | foreach (var record in spreadsheet.ReadWorksheet("Sheet1")) 80 | { 81 | Assert.Equal(record.Bool, testClassWithConstantValues.Bool); 82 | Assert.Equal(record.Byte, testClassWithConstantValues.Byte); 83 | Assert.Equal(record.Char, testClassWithConstantValues.Char); 84 | Assert.Equal(record.DateTime, testClassWithConstantValues.DateTime); 85 | Assert.Equal(record.Decimal, testClassWithConstantValues.Decimal); 86 | Assert.Equal(record.Double, testClassWithConstantValues.Double); 87 | Assert.Equal(record.Float, testClassWithConstantValues.Float); 88 | Assert.Equal(record.Int, testClassWithConstantValues.Int); 89 | Assert.Equal(record.Long, testClassWithConstantValues.Long); 90 | Assert.Equal(record.Text, testClassWithConstantValues.Text); 91 | } 92 | } 93 | 94 | File.Delete(spreadsheetFile); 95 | } 96 | } 97 | 98 | public class TestClass 99 | { 100 | public bool Bool { get; set; } 101 | public byte Byte { get; set; } 102 | public char Char { get; set; } = 'a'; 103 | public DateTime DateTime { get; set; } 104 | public decimal Decimal { get; set; } 105 | public double Double { get; set; } 106 | public float Float { get; set; } 107 | public int Int { get; set; } 108 | public long Long { get; set; } 109 | public string Text { get; set; } = string.Empty; 110 | } 111 | 112 | public class TestClassMapWithConstants : ClassMap 113 | { 114 | public TestClassMapWithConstants() 115 | { 116 | base.Map(x => x.Bool).Index(1).Constant(testClassWithConstantValues.Bool); 117 | base.Map(x => x.Byte).Index(2).Constant(testClassWithConstantValues.Byte); 118 | base.Map(x => x.Char).Index(3).Constant(testClassWithConstantValues.Char); 119 | base.Map(x => x.DateTime).Index(4).Constant(testClassWithConstantValues.DateTime); 120 | base.Map(x => x.Decimal).Index(5).Constant(testClassWithConstantValues.Decimal); 121 | base.Map(x => x.Double).Index(6).Constant(testClassWithConstantValues.Double); 122 | base.Map(x => x.Float).Index(7).Constant(testClassWithConstantValues.Float); 123 | base.Map(x => x.Int).Index(8).Constant(testClassWithConstantValues.Int); 124 | base.Map(x => x.Long).Index(9).Constant(testClassWithConstantValues.Long); 125 | base.Map(x => x.Text).Index(10).Constant(testClassWithConstantValues.Text); 126 | } 127 | } 128 | 129 | public class TestClassMapWithoutConstants : ClassMap 130 | { 131 | public TestClassMapWithoutConstants() 132 | { 133 | base.Map(x => x.Bool).Index(1); 134 | base.Map(x => x.Byte).Index(2); 135 | base.Map(x => x.Char).Index(3); 136 | base.Map(x => x.DateTime).Index(4); 137 | base.Map(x => x.Decimal).Index(5); 138 | base.Map(x => x.Double).Index(6); 139 | base.Map(x => x.Float).Index(7); 140 | base.Map(x => x.Int).Index(8); 141 | base.Map(x => x.Long).Index(9); 142 | base.Map(x => x.Text).Index(10); 143 | } 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /src/Tests/DataConversions/ConvertUsing.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.DataConversions 2 | { 3 | using System.IO; 4 | using OpenSpreadsheet; 5 | using OpenSpreadsheet.Configuration; 6 | using Xunit; 7 | 8 | public class ConvertUsing : SpreadsheetTesterBase 9 | { 10 | [Fact] 11 | public void TestConvertUsing() 12 | { 13 | var filepath = base.ConstructTempXlsxSaveName(); 14 | using (var spreadsheet = new Spreadsheet(filepath)) 15 | { 16 | spreadsheet.WriteWorksheet("Sheet1", base.CreateRecords(10)); 17 | } 18 | 19 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 20 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 21 | { 22 | base.SpreadsheetValidator.Validate(spreadsheetFile); 23 | Assert.False(base.SpreadsheetValidator.HasErrors); 24 | 25 | using (var spreadsheet = new Spreadsheet(spreadsheetFile)) 26 | { 27 | foreach (var record in spreadsheet.ReadWorksheet("Sheet1")) 28 | { 29 | Assert.Equal(TestEnum.A, record.TestEnum1); 30 | Assert.Equal(TestEnum.B, record.TestEnum2); 31 | Assert.Equal(TestEnum.C, record.TestEnum3); 32 | } 33 | } 34 | 35 | File.Delete(spreadsheetFile); 36 | } 37 | } 38 | 39 | private class TestClass 40 | { 41 | public TestEnum TestEnum1 { get; set; } = TestEnum.A; 42 | public TestEnum TestEnum2 { get; set; } = TestEnum.B; 43 | public TestEnum TestEnum3 { get; set; } = TestEnum.C; 44 | } 45 | 46 | private enum TestEnum 47 | { 48 | Unset = 0, 49 | A, 50 | B, 51 | C 52 | } 53 | 54 | private class TestClassMap : ClassMap 55 | { 56 | public TestClassMap() 57 | { 58 | base.Map(x => x.TestEnum1).Name("No Conversion"); 59 | 60 | base.Map(x => x.TestEnum2).Name("Conversion B") 61 | .ReadUsing(row => ConvertStringToEnum(row.GetCellValue("Conversion B"))) 62 | .WriteUsing(x => ConvertEnumToString(x.TestEnum2)); 63 | 64 | base.Map(x => x.TestEnum3).Name("Conversion C") 65 | .ReadUsing(row => ConvertStringToEnum(row.GetCellValue("Conversion C"))) 66 | .WriteUsing(x => ConvertEnumToString(x.TestEnum3)); 67 | } 68 | } 69 | 70 | private static string ConvertEnumToString(TestEnum testEnum) 71 | { 72 | switch (testEnum) 73 | { 74 | case TestEnum.A: 75 | return "Enum A"; 76 | 77 | case TestEnum.B: 78 | return "Enum B"; 79 | 80 | case TestEnum.C: 81 | return "Enum C"; 82 | 83 | default: 84 | return "Undefined"; 85 | } 86 | } 87 | 88 | private static TestEnum ConvertStringToEnum(string enumText) 89 | { 90 | switch (enumText) 91 | { 92 | case "Enum A": 93 | return TestEnum.A; 94 | 95 | case "Enum B": 96 | return TestEnum.B; 97 | 98 | case "Enum C": 99 | return TestEnum.C; 100 | 101 | default: 102 | return TestEnum.Unset; 103 | } 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /src/Tests/DataConversions/Defaults.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.DataConversions 2 | { 3 | using System; 4 | using System.IO; 5 | 6 | using OpenSpreadsheet; 7 | using OpenSpreadsheet.Configuration; 8 | 9 | using Xunit; 10 | 11 | public class Defaults : SpreadsheetTesterBase 12 | { 13 | private static readonly TestClass testClassWithDefaultValues = new TestClass() 14 | { 15 | Bool = true, 16 | Byte = 24, 17 | Char = 'h', 18 | DateTime = new DateTime(2019, 10, 31), 19 | Decimal = 433123.12M, 20 | Double = 6.1E+3, 21 | Float = 23342.93F, 22 | Int = 551412, 23 | Long = 4324823423423, 24 | Text = "default string", 25 | }; 26 | 27 | [Fact] 28 | public void TestRead() 29 | { 30 | var filepath = base.ConstructTempXlsxSaveName(); 31 | using (var spreadsheet = new Spreadsheet(filepath)) 32 | { 33 | spreadsheet.WriteWorksheet("Sheet1", base.CreateRecords(10)); 34 | } 35 | 36 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 37 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 38 | { 39 | base.SpreadsheetValidator.Validate(spreadsheetFile); 40 | Assert.False(base.SpreadsheetValidator.HasErrors); 41 | 42 | using (var spreadsheet = new Spreadsheet(spreadsheetFile)) 43 | { 44 | foreach (var record in spreadsheet.ReadWorksheet("Sheet1")) 45 | { 46 | Assert.Equal(testClassWithDefaultValues.Bool, record.Bool.Value); 47 | Assert.Equal(testClassWithDefaultValues.Byte, record.Byte.Value); 48 | Assert.Equal(testClassWithDefaultValues.Char, record.Char.Value); 49 | Assert.Equal(testClassWithDefaultValues.DateTime, record.DateTime.Value); 50 | Assert.Equal(testClassWithDefaultValues.Decimal, record.Decimal.Value); 51 | Assert.Equal(testClassWithDefaultValues.Double, record.Double.Value); 52 | Assert.Equal(testClassWithDefaultValues.Float, record.Float.Value); 53 | Assert.Equal(testClassWithDefaultValues.Int, record.Int.Value); 54 | Assert.Equal(testClassWithDefaultValues.Long, record.Long.Value); 55 | Assert.Equal(testClassWithDefaultValues.Text, record.Text); 56 | } 57 | } 58 | 59 | File.Delete(spreadsheetFile); 60 | } 61 | } 62 | 63 | [Fact] 64 | public void TestWrite() 65 | { 66 | var filepath = base.ConstructTempXlsxSaveName(); 67 | using (var spreadsheet = new Spreadsheet(filepath)) 68 | { 69 | spreadsheet.WriteWorksheet("Sheet1", base.CreateRecords(10)); 70 | } 71 | 72 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 73 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 74 | { 75 | base.SpreadsheetValidator.Validate(spreadsheetFile); 76 | Assert.False(base.SpreadsheetValidator.HasErrors); 77 | 78 | using (var spreadsheet = new Spreadsheet(spreadsheetFile)) 79 | { 80 | foreach (var record in spreadsheet.ReadWorksheet("Sheet1")) 81 | { 82 | Assert.Equal(testClassWithDefaultValues.Bool, record.Bool.Value); 83 | Assert.Equal(testClassWithDefaultValues.Byte, record.Byte.Value); 84 | Assert.Equal(testClassWithDefaultValues.Char, record.Char.Value); 85 | Assert.Equal(testClassWithDefaultValues.DateTime, record.DateTime.Value); 86 | Assert.Equal(testClassWithDefaultValues.Decimal, record.Decimal.Value); 87 | Assert.Equal(testClassWithDefaultValues.Double, record.Double.Value); 88 | Assert.Equal(testClassWithDefaultValues.Float, record.Float.Value); 89 | Assert.Equal(testClassWithDefaultValues.Int, record.Int.Value); 90 | Assert.Equal(testClassWithDefaultValues.Long, record.Long.Value); 91 | Assert.Equal(testClassWithDefaultValues.Text, record.Text); 92 | } 93 | } 94 | 95 | File.Delete(spreadsheetFile); 96 | } 97 | } 98 | 99 | public class TestClass 100 | { 101 | public bool? Bool { get; set; } 102 | public byte? Byte { get; set; } 103 | public char? Char { get; set; } 104 | public DateTime? DateTime { get; set; } 105 | public decimal? Decimal { get; set; } 106 | public double? Double { get; set; } 107 | public float? Float { get; set; } 108 | public int? Int { get; set; } 109 | public long? Long { get; set; } 110 | public string Text { get; set; } 111 | } 112 | 113 | private class TestClassMap : ClassMap 114 | { 115 | public TestClassMap() 116 | { 117 | base.Map(x => x.Bool).Index(1); 118 | base.Map(x => x.Byte).Index(2); 119 | base.Map(x => x.Char).Index(3); 120 | base.Map(x => x.DateTime).Index(4); 121 | base.Map(x => x.Decimal).Index(5); 122 | base.Map(x => x.Double).Index(6); 123 | base.Map(x => x.Float).Index(7); 124 | base.Map(x => x.Int).Index(8); 125 | base.Map(x => x.Long).Index(9); 126 | base.Map(x => x.Text).Index(10); 127 | } 128 | } 129 | 130 | private class TestClassMapDefaults : ClassMap 131 | { 132 | public TestClassMapDefaults() 133 | { 134 | base.Map(x => x.Bool).Index(1).Default(testClassWithDefaultValues.Bool); 135 | base.Map(x => x.Byte).Index(2).Default(testClassWithDefaultValues.Byte); 136 | base.Map(x => x.Char).Index(3).Default(testClassWithDefaultValues.Char); 137 | base.Map(x => x.DateTime).Index(4).Default(testClassWithDefaultValues.DateTime); 138 | base.Map(x => x.Decimal).Index(5).Default(testClassWithDefaultValues.Decimal); 139 | base.Map(x => x.Double).Index(6).Default(testClassWithDefaultValues.Double); 140 | base.Map(x => x.Float).Index(7).Default(testClassWithDefaultValues.Float); 141 | base.Map(x => x.Int).Index(8).Default(testClassWithDefaultValues.Int); 142 | base.Map(x => x.Long).Index(9).Default(testClassWithDefaultValues.Long); 143 | base.Map(x => x.Text).Index(10).Default(testClassWithDefaultValues.Text); 144 | } 145 | } 146 | } 147 | } -------------------------------------------------------------------------------- /src/Tests/DataConversions/NumberFormats.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.DataConversions 2 | { 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using DocumentFormat.OpenXml.Packaging; 7 | using DocumentFormat.OpenXml.Spreadsheet; 8 | using OpenSpreadsheet; 9 | using OpenSpreadsheet.Configuration; 10 | using OpenSpreadsheet.Enums; 11 | using Xunit; 12 | 13 | public class NumberFormats : SpreadsheetTesterBase 14 | { 15 | private static readonly Dictionary customNumberFormats = new Dictionary() 16 | { 17 | { 1, "\"The number is \"#,##0.00" }, 18 | { 2, "\"$#,##0.00;[Black]($#,##0.00);\"" }, 19 | { 3, "\"$#,##0.00;[Red]$#,##0.00\"" }, 20 | { 4, "\"$#,##0.00;[Black]$-#,##0.00;\"" }, 21 | }; 22 | 23 | private static readonly Dictionary numberFormats = new Dictionary() 24 | { 25 | { 1, OpenXmlNumberingFormat.Accounting }, 26 | { 2, OpenXmlNumberingFormat.Currency }, 27 | { 3, OpenXmlNumberingFormat.DateDayMonth }, 28 | { 4, OpenXmlNumberingFormat.DateDayMonthYear }, 29 | { 5, OpenXmlNumberingFormat.DateMonthDayYear }, 30 | { 6, OpenXmlNumberingFormat.DateMonthYear }, 31 | { 7, OpenXmlNumberingFormat.DatestampWithTime }, 32 | { 8, OpenXmlNumberingFormat.Decimal }, 33 | { 9, OpenXmlNumberingFormat.DecimalWithNegativeInParens }, 34 | { 10, OpenXmlNumberingFormat.DecimalWithNegativeInRedParens }, 35 | { 11, OpenXmlNumberingFormat.FractionOneDigit }, 36 | { 12, OpenXmlNumberingFormat.FractionTwoDigits }, 37 | { 13, OpenXmlNumberingFormat.General }, 38 | { 14, OpenXmlNumberingFormat.Number }, 39 | { 15, OpenXmlNumberingFormat.NumberWithCommas}, 40 | { 16, OpenXmlNumberingFormat.NumberWithNegativeInParens }, 41 | { 17, OpenXmlNumberingFormat.NumberWithNegativeInRedParens }, 42 | { 18, OpenXmlNumberingFormat.Percentage }, 43 | { 19, OpenXmlNumberingFormat.PercentageDecimal}, 44 | { 20, OpenXmlNumberingFormat.Scientific }, 45 | { 21, OpenXmlNumberingFormat.Text }, 46 | { 22, OpenXmlNumberingFormat.TimestampConditionalHourMinuteSecond }, 47 | { 23, OpenXmlNumberingFormat.TimestampHouMinuteSecond12 }, 48 | { 24, OpenXmlNumberingFormat.TimestampHouMinuteSecond24 }, 49 | { 25, OpenXmlNumberingFormat.TimestampHourMinute12 }, 50 | { 26, OpenXmlNumberingFormat.TimestampHourMinute24 }, 51 | { 27, OpenXmlNumberingFormat.TimestampMinuteSecond }, 52 | }; 53 | 54 | [Fact] 55 | public void TestCustomNumberFormat() 56 | { 57 | var filepath = base.ConstructTempXlsxSaveName(); 58 | using (var spreadsheet = new Spreadsheet(filepath)) 59 | { 60 | spreadsheet.WriteWorksheet("Sheet1", base.CreateRecords(10), new WorksheetStyle() { ShouldWriteHeaderRow = false }); 61 | } 62 | 63 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 64 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 65 | { 66 | base.SpreadsheetValidator.Validate(spreadsheetFile); 67 | Assert.False(base.SpreadsheetValidator.HasErrors); 68 | 69 | using (var filestream = new FileStream(spreadsheetFile, FileMode.Open, FileAccess.Read, FileShare.Read)) 70 | using (var spreadsheetDocument = SpreadsheetDocument.Open(filestream, false)) 71 | { 72 | var workbookPart = spreadsheetDocument.WorkbookPart; 73 | var worksheetPart = workbookPart.WorksheetParts.First(); 74 | var sheet = worksheetPart.Worksheet; 75 | 76 | foreach (var cell in sheet.Descendants()) 77 | { 78 | var columnIndex = base.GetColumnIndexFromCellReference(cell.CellReference); 79 | var expectedNumberFormat = customNumberFormats[columnIndex]; 80 | var cellFormat = (CellFormat)workbookPart.WorkbookStylesPart.Stylesheet.CellFormats.ChildElements[(int)cell.StyleIndex.Value]; 81 | var numberingFormat = (NumberingFormat)workbookPart.WorkbookStylesPart.Stylesheet.NumberingFormats.ChildElements.FirstOrDefault(x => ((NumberingFormat)x).NumberFormatId == cellFormat.NumberFormatId); 82 | 83 | Assert.Equal(expectedNumberFormat, numberingFormat.FormatCode); 84 | } 85 | } 86 | 87 | File.Delete(spreadsheetFile); 88 | } 89 | } 90 | 91 | [Fact] 92 | public void TestNumberFormats() 93 | { 94 | var filepath = base.ConstructTempXlsxSaveName(); 95 | using (var spreadsheet = new Spreadsheet(filepath)) 96 | { 97 | spreadsheet.WriteWorksheet("Sheet1", base.CreateRecords(10), new WorksheetStyle() { ShouldWriteHeaderRow = false }); 98 | } 99 | 100 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 101 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 102 | { 103 | base.SpreadsheetValidator.Validate(spreadsheetFile); 104 | Assert.False(base.SpreadsheetValidator.HasErrors); 105 | 106 | using (var filestream = new FileStream(spreadsheetFile, FileMode.Open, FileAccess.Read, FileShare.Read)) 107 | using (var spreadsheetDocument = SpreadsheetDocument.Open(filestream, false)) 108 | { 109 | var workbookPart = spreadsheetDocument.WorkbookPart; 110 | var worksheetPart = workbookPart.WorksheetParts.First(); 111 | var sheet = worksheetPart.Worksheet; 112 | 113 | foreach (var cell in sheet.Descendants()) 114 | { 115 | var columnIndex = base.GetColumnIndexFromCellReference(cell.CellReference); 116 | var expectedNumberFormat = numberFormats[columnIndex]; 117 | var cellFormat = (CellFormat)workbookPart.WorkbookStylesPart.Stylesheet.CellFormats.ChildElements[(int)cell.StyleIndex.Value]; 118 | 119 | Assert.Equal((uint)expectedNumberFormat, (uint)cellFormat.NumberFormatId); 120 | } 121 | } 122 | 123 | File.Delete(spreadsheetFile); 124 | } 125 | } 126 | 127 | public class TestClass 128 | { 129 | public string Text { get; set; } = "32312.54"; 130 | } 131 | 132 | private class TestClassMapCustomNumberFormats : ClassMap 133 | { 134 | public TestClassMapCustomNumberFormats() 135 | { 136 | foreach (var numberFormat in customNumberFormats) 137 | { 138 | base.Map(x => x.Text).IgnoreRead(true).Index(numberFormat.Key).Style(new ColumnStyle() { CustomNumberFormat = numberFormat.Value }); 139 | } 140 | } 141 | } 142 | 143 | private class TestClassMapNumberFormats : ClassMap 144 | { 145 | public TestClassMapNumberFormats() 146 | { 147 | foreach (var numberFormat in numberFormats) 148 | { 149 | base.Map(x => x.Text).IgnoreRead(true).Index(numberFormat.Key).Style(new ColumnStyle() { NumberFormat = numberFormat.Value }); 150 | } 151 | } 152 | } 153 | } 154 | } -------------------------------------------------------------------------------- /src/Tests/SpreadsheetIO/Reader.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.SpreadsheetIO 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Text; 8 | using OpenSpreadsheet; 9 | using OpenSpreadsheet.Configuration; 10 | using Xunit; 11 | 12 | public class Reader : SpreadsheetTesterBase 13 | { 14 | [Fact] 15 | public void TestReadAll() 16 | { 17 | const int recordCount = 100; 18 | 19 | var filepath = base.ConstructTempXlsxSaveName(); 20 | using (var spreadsheet = new Spreadsheet(filepath)) 21 | { 22 | spreadsheet.WriteWorksheet("Sheet1", base.CreateRecords(recordCount)); 23 | } 24 | 25 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 26 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 27 | { 28 | using (var spreadsheet = new Spreadsheet(spreadsheetFile)) 29 | { 30 | var records = spreadsheet.ReadWorksheet("Sheet1").ToList(); 31 | Assert.Equal(recordCount, records.Count); 32 | 33 | foreach (var record in records) 34 | { 35 | Assert.Equal("test data", record.TestData); 36 | } 37 | 38 | using (var reader = spreadsheet.CreateWorksheetReader("Sheet1")) 39 | { 40 | var records2 = spreadsheet.ReadWorksheet("Sheet1").ToList(); 41 | Assert.Equal(recordCount, records2.Count); 42 | 43 | foreach (var record in records2) 44 | { 45 | Assert.Equal("test data", record.TestData); 46 | } 47 | } 48 | } 49 | 50 | File.Delete(spreadsheetFile); 51 | } 52 | 53 | //using (var spreadsheet = new Spreadsheet(this.filepath)) 54 | //{ 55 | // var recordsNoExplicitReader = spreadsheet.ReadWorksheet(allRecordsNoWriterSheetName); 56 | // Assert.Equal(recordCount, recordsNoExplicitReader.Count()); 57 | 58 | // using (var reader = spreadsheet.CreateWorksheetReader(allRecordsWithWriterSheetName)) 59 | // { 60 | // var recordsExplicitReader = reader.ReadRows(); 61 | // Assert.Equal(recordCount, recordsExplicitReader.Count()); 62 | // } 63 | 64 | // using (var reader = spreadsheet.CreateWorksheetReader(skippedRowsSheetName)) 65 | // { 66 | // var firstRow = reader.ReadRow(); 67 | // Assert.Equal("first row", firstRow.TestData); 68 | 69 | // var secondRow = reader.ReadRow(); 70 | // Assert.Equal("second row", secondRow.TestData); 71 | 72 | // reader.SkipRows(rowsToSkip); 73 | 74 | // var thirdRow = reader.ReadRow(); 75 | // Assert.Equal("third row", thirdRow.TestData); 76 | 77 | // var fourthRow = reader.ReadRow(); 78 | // Assert.Equal("fourth row", fourthRow.TestData); 79 | // } 80 | 81 | // var recordsCustomHeaderRow = spreadsheet.ReadWorksheet(headerRowNotDefault, headerRowIndex); 82 | // Assert.Equal(recordCount, recordsCustomHeaderRow.Count()); 83 | //} 84 | } 85 | 86 | private class TestClass 87 | { 88 | public string TestData { get; set; } = "test data"; 89 | } 90 | 91 | private class TestClassMap : ClassMap 92 | { 93 | public TestClassMap() => base.Map(x => x.TestData).Index(1); 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /src/Tests/SpreadsheetIO/Writer.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.SpreadsheetIO 2 | { 3 | using System; 4 | using System.IO; 5 | using System.Linq; 6 | using DocumentFormat.OpenXml.Packaging; 7 | using DocumentFormat.OpenXml.Spreadsheet; 8 | using OpenSpreadsheet; 9 | using OpenSpreadsheet.Configuration; 10 | using Xunit; 11 | 12 | public class Writer : SpreadsheetTesterBase 13 | { 14 | [Fact] 15 | public void AddWorksheetsWithSameName() 16 | { 17 | var records = base.CreateRecords(10); 18 | var filepath = base.ConstructTempXlsxSaveName(); 19 | using (var spreadsheet = new Spreadsheet(filepath)) 20 | { 21 | spreadsheet.WriteWorksheet("Sheet1", base.CreateRecords(10)); 22 | Assert.Throws(() => spreadsheet.WriteWorksheet("Sheet1", records)); 23 | } 24 | } 25 | 26 | [Fact] 27 | public void WriteAllRecords() 28 | { 29 | const int recordCount = 100; 30 | 31 | var records = base.CreateRecords(recordCount); 32 | var filepath = base.ConstructTempXlsxSaveName(); 33 | using (var spreadsheet = new Spreadsheet(filepath)) 34 | { 35 | spreadsheet.WriteWorksheet("Sheet1", records, new WorksheetStyle() { ShouldWriteHeaderRow = false } ); 36 | 37 | using (var explicitWriter = spreadsheet.CreateWorksheetWriter("Sheet2", new WorksheetStyle() { ShouldWriteHeaderRow = false })) 38 | { 39 | foreach (var record in records) 40 | { 41 | explicitWriter.WriteRecord(record); 42 | } 43 | } 44 | } 45 | 46 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 47 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 48 | { 49 | base.SpreadsheetValidator.Validate(spreadsheetFile); 50 | Assert.False(base.SpreadsheetValidator.HasErrors); 51 | 52 | using (var filestream = new FileStream(spreadsheetFile, FileMode.Open, FileAccess.Read, FileShare.Read)) 53 | using (var spreadsheetDocument = SpreadsheetDocument.Open(filestream, false)) 54 | { 55 | var workbookPart = spreadsheetDocument.WorkbookPart; 56 | foreach (var worksheetPart in workbookPart.WorksheetParts) 57 | { 58 | var sheetData = worksheetPart.Worksheet.Elements().FirstOrDefault(); 59 | Assert.Equal(recordCount, sheetData.Elements().Count()); 60 | } 61 | } 62 | 63 | File.Delete(spreadsheetFile); 64 | } 65 | } 66 | 67 | [Fact] 68 | public void TestSkipRows() 69 | { 70 | var filepath = base.ConstructTempXlsxSaveName(); 71 | using (var spreadsheet = new Spreadsheet(filepath)) 72 | using (var writer = spreadsheet.CreateWorksheetWriter("Sheet1", new WorksheetStyle() { ShouldWriteHeaderRow = false })) 73 | { 74 | writer.WriteRecord(new TestClass()); 75 | writer.SkipRows(5); 76 | writer.WriteRecord(new TestClass()); 77 | } 78 | 79 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 80 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 81 | { 82 | base.SpreadsheetValidator.Validate(spreadsheetFile); 83 | Assert.False(base.SpreadsheetValidator.HasErrors); 84 | 85 | using (var filestream = new FileStream(spreadsheetFile, FileMode.Open, FileAccess.Read, FileShare.Read)) 86 | using (var spreadsheetDocument = SpreadsheetDocument.Open(filestream, false)) 87 | { 88 | var workbookPart = spreadsheetDocument.WorkbookPart; 89 | var worksheetPart = workbookPart.WorksheetParts.First(); 90 | var sheet = worksheetPart.Worksheet; 91 | 92 | var rows = sheet.Descendants(); 93 | Assert.Equal(1, rows.ElementAt(0).RowIndex.Value); 94 | Assert.Equal(7, rows.ElementAt(1).RowIndex.Value); 95 | } 96 | 97 | File.Delete(spreadsheetFile); 98 | } 99 | } 100 | 101 | [Fact] 102 | public void TestWorksheetIndex() 103 | { 104 | var records = base.CreateRecords(10); 105 | var filepath = base.ConstructTempXlsxSaveName(); 106 | using (var spreadsheet = new Spreadsheet(filepath)) 107 | { 108 | spreadsheet.WriteWorksheet("Sheet2", records); 109 | spreadsheet.WriteWorksheet("Sheet3", records); 110 | spreadsheet.WriteWorksheet("Sheet1", 0, records); 111 | } 112 | 113 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 114 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 115 | { 116 | base.SpreadsheetValidator.Validate(spreadsheetFile); 117 | Assert.False(base.SpreadsheetValidator.HasErrors); 118 | 119 | using (var filestream = new FileStream(spreadsheetFile, FileMode.Open, FileAccess.Read, FileShare.Read)) 120 | using (var spreadsheetDocument = SpreadsheetDocument.Open(filestream, false)) 121 | { 122 | Assert.Equal("Sheet1", spreadsheetDocument.WorkbookPart.Workbook.Descendants().ElementAt(0).Name); 123 | Assert.Equal("Sheet2", spreadsheetDocument.WorkbookPart.Workbook.Descendants().ElementAt(1).Name); 124 | Assert.Equal("Sheet3", spreadsheetDocument.WorkbookPart.Workbook.Descendants().ElementAt(2).Name); 125 | } 126 | 127 | File.Delete(spreadsheetFile); 128 | } 129 | } 130 | 131 | [Fact] 132 | public void TestWriteHeaders() 133 | { 134 | var filepath = base.ConstructTempXlsxSaveName(); 135 | using (var spreadsheet = new Spreadsheet(filepath)) 136 | using (var writer = spreadsheet.CreateWorksheetWriter("Sheet1", new WorksheetStyle() { ShouldWriteHeaderRow = false })) 137 | { 138 | writer.WriteHeader(); 139 | writer.WriteHeader(); 140 | writer.WriteHeader(); 141 | } 142 | 143 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 144 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 145 | { 146 | base.SpreadsheetValidator.Validate(spreadsheetFile); 147 | Assert.False(base.SpreadsheetValidator.HasErrors); 148 | 149 | using (var filestream = new FileStream(spreadsheetFile, FileMode.Open, FileAccess.Read, FileShare.Read)) 150 | using (var spreadsheetDocument = SpreadsheetDocument.Open(filestream, false)) 151 | { 152 | var workbookPart = spreadsheetDocument.WorkbookPart; 153 | var worksheetPart = workbookPart.WorksheetParts.First(); 154 | var sheet = worksheetPart.Worksheet; 155 | var sharedStringPart = workbookPart.SharedStringTablePart; 156 | 157 | foreach (var cell in sheet.Descendants()) 158 | { 159 | var cellValue = base.GetSharedStringValue(sharedStringPart, cell.CellValue.InnerText); 160 | Assert.Equal("TestData", cellValue); 161 | } 162 | } 163 | 164 | File.Delete(spreadsheetFile); 165 | } 166 | } 167 | 168 | private class TestClass 169 | { 170 | public string TestData { get; set; } = "test data"; 171 | } 172 | 173 | private class TestClassMap : ClassMap 174 | { 175 | public TestClassMap() => base.Map(x => x.TestData).Index(1); 176 | } 177 | } 178 | } -------------------------------------------------------------------------------- /src/Tests/SpreadsheetTesterBase.cs: -------------------------------------------------------------------------------- 1 | namespace Tests 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Drawing; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Runtime.InteropServices; 9 | using DocumentFormat.OpenXml.Packaging; 10 | using Excel = Microsoft.Office.Interop.Excel; 11 | 12 | public abstract class SpreadsheetTesterBase 13 | { 14 | protected SpreadsheetValidator SpreadsheetValidator { get; } = new SpreadsheetValidator(); 15 | 16 | public string ConstructTempXlsxSaveName() => Path.Combine(Path.GetTempPath(), Path.ChangeExtension(Path.GetRandomFileName(), ".xlsx")); 17 | 18 | public uint GetColumnIndexFromCellReference(string cellReference) 19 | { 20 | var columnLetters = cellReference.Where(c => !char.IsNumber(c)).ToArray(); 21 | string columnReference = new string(columnLetters); 22 | 23 | int sum = 0; 24 | for (int i = 0; i < columnLetters.Length; i++) 25 | { 26 | sum *= 26; 27 | sum += columnLetters[i] - 'A' + 1; 28 | } 29 | 30 | return (uint)sum; 31 | } 32 | 33 | public string ConvertColorToHex(in Color color) => Color.FromArgb(color.ToArgb()).Name; 34 | 35 | public IEnumerable CreateRecords(int count) where T : class 36 | { 37 | for (int i = 0; i < count; i++) 38 | { 39 | yield return Activator.CreateInstance(); 40 | } 41 | } 42 | 43 | public string GetSharedStringValue(SharedStringTablePart sharedStringTablePart, string cellValue) => 44 | sharedStringTablePart.SharedStringTable.ElementAt(int.Parse(cellValue)).InnerText; 45 | 46 | public string SaveAsExcelFile(string spreadsheetFile) 47 | { 48 | var excelFileName = this.ConstructTempXlsxSaveName(); 49 | var excel = new Excel.Application(); 50 | var workbook = excel.Workbooks.Open(spreadsheetFile); 51 | workbook.SaveAs(excelFileName); 52 | workbook.Close(); 53 | excel.Quit(); 54 | Marshal.ReleaseComObject(workbook); 55 | Marshal.ReleaseComObject(excel); 56 | 57 | return excelFileName; 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/Tests/SpreadsheetValidator.cs: -------------------------------------------------------------------------------- 1 | namespace Tests 2 | { 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | using DocumentFormat.OpenXml.Packaging; 7 | using DocumentFormat.OpenXml.Validation; 8 | 9 | public class SpreadsheetValidator 10 | { 11 | public IList Errors { get; private set; } = new List(); 12 | 13 | public bool HasErrors => this.Errors.Count > 0; 14 | 15 | public void Validate(string spreadsheetFile) 16 | { 17 | using (var spreadsheet = SpreadsheetDocument.Open(spreadsheetFile, false)) 18 | { 19 | var validator = new OpenXmlValidator(); 20 | this.Errors = validator.Validate(spreadsheet).ToList(); 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/Tests/Styles/Alignments.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Styles 2 | { 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using DocumentFormat.OpenXml.Packaging; 7 | using DocumentFormat.OpenXml.Spreadsheet; 8 | using OpenSpreadsheet; 9 | using OpenSpreadsheet.Configuration; 10 | using Xunit; 11 | 12 | public class Alignments : SpreadsheetTesterBase 13 | { 14 | private static readonly Dictionary horizontalAlignments = new Dictionary() 15 | { 16 | { 1, HorizontalAlignmentValues.Center }, 17 | { 2, HorizontalAlignmentValues.CenterContinuous }, 18 | { 3, HorizontalAlignmentValues.Distributed }, 19 | { 4, HorizontalAlignmentValues.Fill }, 20 | { 5, HorizontalAlignmentValues.General }, 21 | { 6, HorizontalAlignmentValues.Justify }, 22 | { 7, HorizontalAlignmentValues.Left }, 23 | { 8, HorizontalAlignmentValues.Right }, 24 | }; 25 | 26 | private static readonly Dictionary verticalAlignments = new Dictionary() 27 | { 28 | { 1, VerticalAlignmentValues.Bottom }, 29 | { 2, VerticalAlignmentValues.Center }, 30 | { 3, VerticalAlignmentValues.Distributed }, 31 | { 4, VerticalAlignmentValues.Justify }, 32 | { 5, VerticalAlignmentValues.Top }, 33 | }; 34 | 35 | [Fact] 36 | public void TestHorizontalAlignments() 37 | { 38 | var filepath = base.ConstructTempXlsxSaveName(); 39 | using (var spreadsheet = new Spreadsheet(filepath)) 40 | { 41 | spreadsheet.WriteWorksheet("Sheet1", base.CreateRecords(10), new WorksheetStyle() { ShouldWriteHeaderRow = false }); 42 | } 43 | 44 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 45 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 46 | { 47 | base.SpreadsheetValidator.Validate(spreadsheetFile); 48 | Assert.False(base.SpreadsheetValidator.HasErrors); 49 | 50 | using (var filestream = new FileStream(spreadsheetFile, FileMode.Open, FileAccess.Read, FileShare.Read)) 51 | using (var spreadsheetDocument = SpreadsheetDocument.Open(filestream, false)) 52 | { 53 | var workbookPart = spreadsheetDocument.WorkbookPart; 54 | var worksheetPart = workbookPart.WorksheetParts.First(); 55 | var sheet = worksheetPart.Worksheet; 56 | 57 | foreach (var cell in sheet.Descendants()) 58 | { 59 | var columnIndex = base.GetColumnIndexFromCellReference(cell.CellReference); 60 | var expectedAlignment = horizontalAlignments[columnIndex]; 61 | 62 | var cellFormat = (CellFormat)workbookPart.WorkbookStylesPart.Stylesheet.CellFormats.ChildElements[(int)cell.StyleIndex.Value]; 63 | 64 | // excel removes the general horizontal alignment value attribute 65 | if (expectedAlignment == HorizontalAlignmentValues.General) 66 | { 67 | Assert.True(cellFormat.Alignment.Horizontal == null || cellFormat.Alignment.Horizontal == HorizontalAlignmentValues.General); 68 | } 69 | else 70 | { 71 | Assert.Equal(expectedAlignment, cellFormat.Alignment.Horizontal); 72 | } 73 | } 74 | } 75 | 76 | File.Delete(spreadsheetFile); 77 | } 78 | } 79 | 80 | [Fact] 81 | public void TestVerticalAlignments() 82 | { 83 | var filepath = base.ConstructTempXlsxSaveName(); 84 | using (var spreadsheet = new Spreadsheet(filepath)) 85 | { 86 | spreadsheet.WriteWorksheet("Sheet1", base.CreateRecords(10), new WorksheetStyle() { ShouldWriteHeaderRow = false }); 87 | } 88 | 89 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 90 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 91 | { 92 | base.SpreadsheetValidator.Validate(spreadsheetFile); 93 | Assert.False(base.SpreadsheetValidator.HasErrors); 94 | 95 | using (var filestream = new FileStream(spreadsheetFile, FileMode.Open, FileAccess.Read, FileShare.Read)) 96 | using (var spreadsheetDocument = SpreadsheetDocument.Open(filestream, false)) 97 | { 98 | var workbookPart = spreadsheetDocument.WorkbookPart; 99 | var worksheetPart = workbookPart.WorksheetParts.First(); 100 | var sheet = worksheetPart.Worksheet; 101 | 102 | foreach (var cell in sheet.Descendants()) 103 | { 104 | var columnIndex = base.GetColumnIndexFromCellReference(cell.CellReference); 105 | var expectedAlignment = verticalAlignments[columnIndex]; 106 | var cellFormat = (CellFormat)workbookPart.WorkbookStylesPart.Stylesheet.CellFormats.ChildElements[(int)cell.StyleIndex.Value]; 107 | 108 | // excel removes vertical alignment value attribute when it is set to bottom (default) 109 | if (expectedAlignment == VerticalAlignmentValues.Bottom) 110 | { 111 | Assert.True(cellFormat.Alignment == null || cellFormat.Alignment.Vertical == VerticalAlignmentValues.Bottom); 112 | } 113 | else 114 | { 115 | Assert.Equal(expectedAlignment, cellFormat.Alignment.Vertical); 116 | } 117 | } 118 | } 119 | 120 | File.Delete(spreadsheetFile); 121 | } 122 | } 123 | 124 | private class TestClass 125 | { 126 | public string TestData { get; set; } = "test data"; 127 | } 128 | 129 | private class TestClassMapHorizontalAlignments : ClassMap 130 | { 131 | public TestClassMapHorizontalAlignments() 132 | { 133 | foreach (var alignment in horizontalAlignments) 134 | { 135 | base.Map(x => x.TestData).IgnoreRead(true).Index(alignment.Key).Style(new ColumnStyle() { HoizontalAlignment = alignment.Value }); 136 | } 137 | } 138 | } 139 | 140 | private class TestClassMapVerticalAlignments : ClassMap 141 | { 142 | public TestClassMapVerticalAlignments() 143 | { 144 | foreach (var alignment in verticalAlignments) 145 | { 146 | base.Map(x => x.TestData).IgnoreRead(true).Index(alignment.Key).Style(new ColumnStyle() { VerticalAlignment = alignment.Value }); 147 | } 148 | } 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /src/Tests/Styles/BorderStyles.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Styles 2 | { 3 | using System.Collections.Generic; 4 | using System.Drawing; 5 | using System.IO; 6 | using System.Linq; 7 | using DocumentFormat.OpenXml.Packaging; 8 | using OpenSpreadsheet; 9 | using OpenSpreadsheet.Configuration; 10 | using OpenSpreadsheet.Enums; 11 | using Xunit; 12 | 13 | using OpenXml = DocumentFormat.OpenXml.Spreadsheet; 14 | 15 | public class BorderStyles : SpreadsheetTesterBase 16 | { 17 | private static readonly Dictionary borderColors = new Dictionary() 18 | { 19 | { 1, Color.Chocolate }, 20 | { 2, Color.Teal }, 21 | { 3, Color.Black }, 22 | { 4, Color.White }, 23 | { 5, Color.BurlyWood } 24 | }; 25 | 26 | private static readonly Dictionary borderPlacements = new Dictionary() 27 | { 28 | { 1, BorderPlacement.All }, 29 | { 2, BorderPlacement.Bottom }, 30 | { 3, BorderPlacement.DiagonalDown }, 31 | { 4, BorderPlacement.DiagonalUp }, 32 | { 5, BorderPlacement.Left}, 33 | { 6, BorderPlacement.Outside }, 34 | { 7, BorderPlacement.Right }, 35 | { 8, BorderPlacement.Top }, 36 | { 9, BorderPlacement.Bottom | BorderPlacement.Top }, 37 | { 10, BorderPlacement.DiagonalDown | BorderPlacement.DiagonalUp }, 38 | }; 39 | 40 | private static readonly Dictionary borderStyles = new Dictionary() 41 | { 42 | { 1, OpenXml.BorderStyleValues.None }, 43 | { 2, OpenXml.BorderStyleValues.DashDot }, 44 | { 3, OpenXml.BorderStyleValues.DashDotDot }, 45 | { 4, OpenXml.BorderStyleValues.Dashed }, 46 | { 5, OpenXml.BorderStyleValues.Dotted }, 47 | { 6, OpenXml.BorderStyleValues.Double }, 48 | { 7, OpenXml.BorderStyleValues.Hair }, 49 | { 8, OpenXml.BorderStyleValues.Medium }, 50 | { 9, OpenXml.BorderStyleValues.MediumDashDot }, 51 | { 10, OpenXml.BorderStyleValues.MediumDashDotDot }, 52 | { 11, OpenXml.BorderStyleValues.MediumDashed }, 53 | { 12, OpenXml.BorderStyleValues.SlantDashDot }, 54 | { 13, OpenXml.BorderStyleValues.Thick}, 55 | { 14, OpenXml.BorderStyleValues.Thin}, 56 | }; 57 | 58 | [Fact] 59 | public void TestBorderColors() 60 | { 61 | var filepath = base.ConstructTempXlsxSaveName(); 62 | using (var spreadsheet = new Spreadsheet(filepath)) 63 | { 64 | spreadsheet.WriteWorksheet("Sheet1", base.CreateRecords(10), new WorksheetStyle() { ShouldWriteHeaderRow = false }); 65 | } 66 | 67 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 68 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 69 | { 70 | base.SpreadsheetValidator.Validate(spreadsheetFile); 71 | Assert.False(base.SpreadsheetValidator.HasErrors); 72 | 73 | using (var filestream = new FileStream(spreadsheetFile, FileMode.Open, FileAccess.Read, FileShare.Read)) 74 | using (var spreadsheetDocument = SpreadsheetDocument.Open(filestream, false)) 75 | { 76 | var workbookPart = spreadsheetDocument.WorkbookPart; 77 | var worksheetPart = workbookPart.WorksheetParts.First(); 78 | var sheet = worksheetPart.Worksheet; 79 | 80 | foreach (var cell in sheet.Descendants()) 81 | { 82 | var columnIndex = base.GetColumnIndexFromCellReference(cell.CellReference); 83 | var expectedColor = base.ConvertColorToHex(borderColors[columnIndex]); 84 | var border = (OpenXml.Border)workbookPart.WorkbookStylesPart.Stylesheet.Borders.ChildElements[(int)columnIndex]; 85 | 86 | Assert.Equal(expectedColor, border.BottomBorder.Color.Rgb.Value, true); 87 | Assert.Equal(expectedColor, border.DiagonalBorder.Color.Rgb.Value, true); 88 | Assert.Equal(expectedColor, border.LeftBorder.Color.Rgb.Value, true); 89 | Assert.Equal(expectedColor, border.RightBorder.Color.Rgb.Value, true); 90 | Assert.Equal(expectedColor, border.TopBorder.Color.Rgb.Value, true); 91 | } 92 | } 93 | 94 | File.Delete(spreadsheetFile); 95 | } 96 | } 97 | 98 | [Fact] 99 | public void TestBorderPlacements() 100 | { 101 | var filepath = base.ConstructTempXlsxSaveName(); 102 | using (var spreadsheet = new Spreadsheet(filepath)) 103 | { 104 | spreadsheet.WriteWorksheet("Sheet1", base.CreateRecords(10), new WorksheetStyle() { ShouldWriteHeaderRow = false }); 105 | } 106 | 107 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 108 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 109 | { 110 | base.SpreadsheetValidator.Validate(spreadsheetFile); 111 | Assert.False(base.SpreadsheetValidator.HasErrors); 112 | 113 | using (var filestream = new FileStream(spreadsheetFile, FileMode.Open, FileAccess.Read, FileShare.Read)) 114 | using (var spreadsheetDocument = SpreadsheetDocument.Open(filestream, false)) 115 | { 116 | var workbookPart = spreadsheetDocument.WorkbookPart; 117 | var worksheetPart = workbookPart.WorksheetParts.First(); 118 | var sheet = worksheetPart.Worksheet; 119 | 120 | foreach (var cell in sheet.Descendants()) 121 | { 122 | var columnIndex = base.GetColumnIndexFromCellReference(cell.CellReference); 123 | var expectedBorderPlacement = borderPlacements[columnIndex]; 124 | var border = (OpenXml.Border)workbookPart.WorkbookStylesPart.Stylesheet.Borders.ChildElements[(int)columnIndex]; 125 | 126 | if (border.BottomBorder?.HasChildren == true) 127 | { 128 | Assert.True((expectedBorderPlacement & BorderPlacement.Bottom) != 0); 129 | } 130 | 131 | if (border.DiagonalDown?.HasValue == true) 132 | { 133 | Assert.Equal((expectedBorderPlacement & BorderPlacement.DiagonalDown) != 0, border.DiagonalDown.Value); 134 | } 135 | 136 | if (border.DiagonalUp?.HasValue == true) 137 | { 138 | Assert.Equal((expectedBorderPlacement & BorderPlacement.DiagonalUp) != 0, border.DiagonalUp.Value); 139 | } 140 | 141 | if (border.LeftBorder?.HasChildren == true) 142 | { 143 | Assert.True((expectedBorderPlacement & BorderPlacement.Left) != 0); 144 | } 145 | 146 | if (border.RightBorder?.HasChildren == true) 147 | { 148 | Assert.True((expectedBorderPlacement & BorderPlacement.Right) != 0); 149 | } 150 | 151 | if (border.TopBorder?.HasChildren == true) 152 | { 153 | Assert.True((expectedBorderPlacement & BorderPlacement.Top) != 0); 154 | } 155 | } 156 | } 157 | 158 | File.Delete(spreadsheetFile); 159 | } 160 | } 161 | 162 | [Fact] 163 | public void TestBorderStyles() 164 | { 165 | var filepath = base.ConstructTempXlsxSaveName(); 166 | using (var spreadsheet = new Spreadsheet(filepath)) 167 | { 168 | spreadsheet.WriteWorksheet("Sheet1", base.CreateRecords(10), new WorksheetStyle() { ShouldWriteHeaderRow = false }); 169 | } 170 | 171 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 172 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 173 | { 174 | base.SpreadsheetValidator.Validate(spreadsheetFile); 175 | Assert.False(base.SpreadsheetValidator.HasErrors); 176 | 177 | using (var filestream = new FileStream(spreadsheetFile, FileMode.Open, FileAccess.Read, FileShare.Read)) 178 | using (var spreadsheetDocument = SpreadsheetDocument.Open(filestream, false)) 179 | { 180 | var workbookPart = spreadsheetDocument.WorkbookPart; 181 | var worksheetPart = workbookPart.WorksheetParts.First(); 182 | var sheet = worksheetPart.Worksheet; 183 | 184 | foreach (var cell in sheet.Descendants()) 185 | { 186 | var columnIndex = base.GetColumnIndexFromCellReference(cell.CellReference); 187 | var expectedBorderStyle = borderStyles[columnIndex]; 188 | var border = (OpenXml.Border)workbookPart.WorkbookStylesPart.Stylesheet.Borders.ChildElements[(int)columnIndex]; 189 | 190 | if (expectedBorderStyle == OpenXml.BorderStyleValues.None) 191 | { 192 | Assert.True(border.BottomBorder.Style == null || border.BottomBorder.Style == OpenXml.BorderStyleValues.None); 193 | Assert.True(border.DiagonalBorder.Style == null || border.BottomBorder.Style == OpenXml.BorderStyleValues.None); 194 | Assert.True(border.LeftBorder.Style == null || border.BottomBorder.Style == OpenXml.BorderStyleValues.None); 195 | Assert.True(border.RightBorder.Style == null || border.BottomBorder.Style == OpenXml.BorderStyleValues.None); 196 | Assert.True(border.TopBorder.Style == null || border.BottomBorder.Style == OpenXml.BorderStyleValues.None); 197 | } 198 | else 199 | { 200 | Assert.Equal(expectedBorderStyle, border.BottomBorder.Style); 201 | Assert.Equal(expectedBorderStyle, border.DiagonalBorder.Style); 202 | Assert.Equal(expectedBorderStyle, border.LeftBorder.Style); 203 | Assert.Equal(expectedBorderStyle, border.RightBorder.Style); 204 | Assert.Equal(expectedBorderStyle, border.TopBorder.Style); 205 | } 206 | } 207 | } 208 | 209 | File.Delete(spreadsheetFile); 210 | } 211 | } 212 | 213 | private class TestClass 214 | { 215 | public string TestData { get; set; } = "test data"; 216 | } 217 | 218 | private class TestClassMapBorderColors : ClassMap 219 | { 220 | public TestClassMapBorderColors() 221 | { 222 | foreach (var color in borderColors) 223 | { 224 | base.Map(x => x.TestData).IgnoreRead(true).Index(color.Key).Style(new ColumnStyle() { BorderColor = color.Value, BorderPlacement = BorderPlacement.All, BorderStyle = OpenXml.BorderStyleValues.Thick }); 225 | } 226 | } 227 | } 228 | 229 | private class TestClassMapBorderPlacements : ClassMap 230 | { 231 | public TestClassMapBorderPlacements() 232 | { 233 | foreach (var borderPlacement in borderPlacements) 234 | { 235 | base.Map(x => x.TestData).IgnoreRead(true).Index(borderPlacement.Key).Style(new ColumnStyle() { BorderPlacement = borderPlacement.Value, BorderStyle = OpenXml.BorderStyleValues.Thick }); 236 | } 237 | } 238 | } 239 | 240 | private class TestClassMapBorderStyles : ClassMap 241 | { 242 | public TestClassMapBorderStyles() 243 | { 244 | foreach (var borderStyle in borderStyles) 245 | { 246 | base.Map(x => x.TestData).IgnoreRead(true).Index(borderStyle.Key).Style(new ColumnStyle() { BorderPlacement = OpenSpreadsheet.Enums.BorderPlacement.All, BorderStyle = borderStyle.Value }); 247 | } 248 | } 249 | } 250 | } 251 | } -------------------------------------------------------------------------------- /src/Tests/Styles/CellPattern.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Styles 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Drawing; 6 | using System.IO; 7 | using System.Linq; 8 | using DocumentFormat.OpenXml.Packaging; 9 | using OpenSpreadsheet; 10 | using OpenSpreadsheet.Configuration; 11 | using Xunit; 12 | 13 | using OpenXml = DocumentFormat.OpenXml.Spreadsheet; 14 | 15 | public class CellPattern : SpreadsheetTesterBase 16 | { 17 | // Fill index begins at 2, following default system fills. 18 | private static readonly Dictionary backgrounds = new Dictionary() 19 | { 20 | { 2, Color.Chocolate }, 21 | { 3, Color.Teal }, 22 | { 4, Color.Black }, 23 | { 5, Color.White }, 24 | { 6, Color.BurlyWood }, 25 | }; 26 | 27 | // skips OpenXml.PatternValues.Gray125 }, system pattern at index 1 28 | private static readonly Dictionary patterns = new Dictionary() 29 | { 30 | { 2, OpenXml.PatternValues.DarkDown }, 31 | { 3, OpenXml.PatternValues.DarkGray }, 32 | { 4, OpenXml.PatternValues.DarkGrid }, 33 | { 5, OpenXml.PatternValues.DarkHorizontal }, 34 | { 6, OpenXml.PatternValues.DarkTrellis }, 35 | { 7, OpenXml.PatternValues.DarkUp }, 36 | { 8, OpenXml.PatternValues.DarkVertical }, 37 | { 9, OpenXml.PatternValues.Gray0625 }, 38 | { 10, OpenXml.PatternValues.LightDown }, 39 | { 11, OpenXml.PatternValues.LightGray }, 40 | { 12, OpenXml.PatternValues.LightGrid }, 41 | { 13, OpenXml.PatternValues.LightHorizontal }, 42 | { 14, OpenXml.PatternValues.LightTrellis }, 43 | { 15, OpenXml.PatternValues.LightUp }, 44 | { 16, OpenXml.PatternValues.LightVertical }, 45 | { 17, OpenXml.PatternValues.MediumGray }, 46 | { 18, OpenXml.PatternValues.None }, 47 | { 19, OpenXml.PatternValues.Solid }, 48 | }; 49 | 50 | [Fact] 51 | public void TestBackgrounds() 52 | { 53 | var filepath = base.ConstructTempXlsxSaveName(); 54 | using (var spreadsheet = new Spreadsheet(filepath)) 55 | { 56 | spreadsheet.WriteWorksheet("Sheet1", base.CreateRecords(10), new WorksheetStyle() { ShouldWriteHeaderRow = false }); 57 | } 58 | 59 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 60 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 61 | { 62 | base.SpreadsheetValidator.Validate(spreadsheetFile); 63 | Assert.False(base.SpreadsheetValidator.HasErrors); 64 | 65 | using (var filestream = new FileStream(spreadsheetFile, FileMode.Open, FileAccess.Read, FileShare.Read)) 66 | using (var spreadsheetDocument = SpreadsheetDocument.Open(filestream, false)) 67 | { 68 | var workbookPart = spreadsheetDocument.WorkbookPart; 69 | var worksheetPart = workbookPart.WorksheetParts.First(); 70 | var sheet = worksheetPart.Worksheet; 71 | 72 | foreach (var cell in sheet.Descendants()) 73 | { 74 | var columnIndex = base.GetColumnIndexFromCellReference(cell.CellReference); 75 | var expectedColor = backgrounds[columnIndex]; 76 | var fill = (OpenXml.Fill)workbookPart.WorkbookStylesPart.Stylesheet.Fills.ChildElements[(int)columnIndex]; 77 | 78 | Assert.Equal(base.ConvertColorToHex(expectedColor), fill.PatternFill.ForegroundColor.Rgb.Value, true); 79 | } 80 | } 81 | 82 | File.Delete(spreadsheetFile); 83 | } 84 | } 85 | 86 | [Fact] 87 | public void TestPatterns() 88 | { 89 | var filepath = base.ConstructTempXlsxSaveName(); 90 | using (var spreadsheet = new Spreadsheet(filepath)) 91 | { 92 | spreadsheet.WriteWorksheet("Sheet1", base.CreateRecords(10), new WorksheetStyle() { ShouldWriteHeaderRow = false }); 93 | } 94 | 95 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 96 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 97 | { 98 | base.SpreadsheetValidator.Validate(spreadsheetFile); 99 | Assert.False(base.SpreadsheetValidator.HasErrors); 100 | 101 | using (var filestream = new FileStream(spreadsheetFile, FileMode.Open, FileAccess.Read, FileShare.Read)) 102 | using (var spreadsheetDocument = SpreadsheetDocument.Open(filestream, false)) 103 | { 104 | var workbookPart = spreadsheetDocument.WorkbookPart; 105 | var worksheetPart = workbookPart.WorksheetParts.First(); 106 | var sheet = worksheetPart.Worksheet; 107 | 108 | foreach (var cell in sheet.Descendants()) 109 | { 110 | var columnIndex = base.GetColumnIndexFromCellReference(cell.CellReference); 111 | var expectedPattern = patterns[columnIndex]; 112 | var fill = (OpenXml.Fill)workbookPart.WorkbookStylesPart.Stylesheet.Fills.ChildElements[(int)columnIndex]; 113 | 114 | Assert.Equal(expectedPattern, fill.PatternFill.PatternType); 115 | } 116 | } 117 | 118 | File.Delete(spreadsheetFile); 119 | } 120 | } 121 | 122 | private class TestClass 123 | { 124 | public string TestData { get; set; } = "test data"; 125 | } 126 | 127 | private class TestClassMapBackgrounds: ClassMap 128 | { 129 | public TestClassMapBackgrounds() 130 | { 131 | foreach (var color in backgrounds) 132 | { 133 | base.Map(x => x.TestData).IgnoreRead(true).Index(color.Key).Style(new ColumnStyle() { BackgroundColor = color.Value }); 134 | } 135 | } 136 | } 137 | 138 | private class TestClassMapPatterns : ClassMap 139 | { 140 | public TestClassMapPatterns() 141 | { 142 | foreach (var pattern in patterns) 143 | { 144 | base.Map(x => x.TestData).IgnoreRead(true).Index(pattern.Key).Style(new ColumnStyle() { BackgroundColor = Color.DarkBlue, BackgroundPatternType = pattern.Value }); 145 | } 146 | } 147 | } 148 | } 149 | } -------------------------------------------------------------------------------- /src/Tests/Styles/Fonts.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Styles 2 | { 3 | using System.Collections.Generic; 4 | using System.Drawing; 5 | using System.IO; 6 | using System.Linq; 7 | using DocumentFormat.OpenXml.Packaging; 8 | using OpenSpreadsheet; 9 | using OpenSpreadsheet.Configuration; 10 | using Xunit; 11 | 12 | using OpenXml = DocumentFormat.OpenXml.Spreadsheet; 13 | 14 | public class Fonts : SpreadsheetTesterBase 15 | { 16 | private static readonly Dictionary fonts = new Dictionary() 17 | { 18 | { 1, new Font("Garamond", 12, FontStyle.Regular) }, 19 | { 2, new Font("Garamond", 12, FontStyle.Underline) }, 20 | { 3, new Font("Times New Roman", 12, FontStyle.Italic) }, 21 | { 4, new Font("Wingdings", 14, FontStyle.Strikeout) }, 22 | { 5, new Font( FontFamily.GenericMonospace, 35, FontStyle.Bold) }, 23 | { 6, new Font( FontFamily.Families[10], 4, FontStyle.Bold | FontStyle.Underline | FontStyle.Italic ) }, 24 | }; 25 | 26 | private static readonly Dictionary foregroundColors = new Dictionary() 27 | { 28 | { 1, Color.Chocolate }, 29 | { 2, Color.Teal }, 30 | { 3, Color.Black }, 31 | { 4, Color.White }, 32 | { 5, Color.BurlyWood } 33 | }; 34 | 35 | [Fact] 36 | public void TestFonts() 37 | { 38 | var filepath = base.ConstructTempXlsxSaveName(); 39 | using (var spreadsheet = new Spreadsheet(filepath)) 40 | { 41 | spreadsheet.WriteWorksheet("Sheet1", base.CreateRecords(10), new WorksheetStyle() { ShouldWriteHeaderRow = false }); 42 | } 43 | 44 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 45 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 46 | { 47 | base.SpreadsheetValidator.Validate(spreadsheetFile); 48 | Assert.False(base.SpreadsheetValidator.HasErrors); 49 | 50 | using (var filestream = new FileStream(spreadsheetFile, FileMode.Open, FileAccess.Read, FileShare.Read)) 51 | using (var spreadsheetDocument = SpreadsheetDocument.Open(filestream, false)) 52 | { 53 | var workbookPart = spreadsheetDocument.WorkbookPart; 54 | var worksheetPart = workbookPart.WorksheetParts.First(); 55 | var sheet = worksheetPart.Worksheet; 56 | 57 | foreach (var cell in sheet.Descendants()) 58 | { 59 | var columnIndex = base.GetColumnIndexFromCellReference(cell.CellReference); 60 | var expectedFont = fonts[columnIndex]; 61 | var cellFont = (OpenXml.Font)workbookPart.WorkbookStylesPart.Stylesheet.Fonts.ChildElements[(int)cell.StyleIndex.Value]; 62 | 63 | if (cellFont.Bold != null) 64 | { 65 | Assert.True(expectedFont.Bold); 66 | } 67 | 68 | if (cellFont.Italic != null) 69 | { 70 | Assert.True(expectedFont.Italic); 71 | } 72 | 73 | if (cellFont.Strike != null) 74 | { 75 | Assert.True(expectedFont.Strikeout); 76 | } 77 | 78 | if (cellFont.Underline != null) 79 | { 80 | Assert.True(expectedFont.Underline); 81 | } 82 | 83 | Assert.Equal(expectedFont.FontFamily.Name, cellFont.FontName.Val); 84 | Assert.Equal(expectedFont.Size, cellFont.FontSize.Val); 85 | } 86 | } 87 | 88 | File.Delete(spreadsheetFile); 89 | } 90 | } 91 | 92 | [Fact] 93 | public void TestForegrounds() 94 | { 95 | var filepath = base.ConstructTempXlsxSaveName(); 96 | using (var spreadsheet = new Spreadsheet(filepath)) 97 | { 98 | spreadsheet.WriteWorksheet("Sheet1", base.CreateRecords(10), new WorksheetStyle() { ShouldWriteHeaderRow = false }); 99 | } 100 | 101 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 102 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 103 | { 104 | base.SpreadsheetValidator.Validate(spreadsheetFile); 105 | Assert.False(base.SpreadsheetValidator.HasErrors); 106 | 107 | using (var filestream = new FileStream(spreadsheetFile, FileMode.Open, FileAccess.Read, FileShare.Read)) 108 | using (var spreadsheetDocument = SpreadsheetDocument.Open(filestream, false)) 109 | { 110 | var workbookPart = spreadsheetDocument.WorkbookPart; 111 | var worksheetPart = workbookPart.WorksheetParts.First(); 112 | var sheet = worksheetPart.Worksheet; 113 | 114 | foreach (var cell in sheet.Descendants()) 115 | { 116 | var columnIndex = base.GetColumnIndexFromCellReference(cell.CellReference); 117 | var expectedForeground = foregroundColors[columnIndex]; 118 | var cellFont = (OpenXml.Font)workbookPart.WorkbookStylesPart.Stylesheet.Fonts.ChildElements[(int)cell.StyleIndex.Value]; 119 | 120 | Assert.Equal(base.ConvertColorToHex(expectedForeground), cellFont.Color.Rgb.Value, true); 121 | } 122 | } 123 | 124 | File.Delete(spreadsheetFile); 125 | } 126 | } 127 | 128 | private class TestClass 129 | { 130 | public string TestData { get; set; } = "test data"; 131 | } 132 | 133 | private class TestClassMapFonts : ClassMap 134 | { 135 | public TestClassMapFonts() 136 | { 137 | foreach (var font in fonts) 138 | { 139 | base.Map(x => x.TestData).IgnoreRead(true).Index(font.Key).Style(new ColumnStyle() { Font = font.Value }); 140 | } 141 | } 142 | } 143 | 144 | private class TestClassMapForegrounds : ClassMap 145 | { 146 | public TestClassMapForegrounds() 147 | { 148 | foreach (var foregroundColor in foregroundColors) 149 | { 150 | base.Map(x => x.TestData).IgnoreRead(true).Index(foregroundColor.Key).Style(new ColumnStyle() { ForegroundColor = foregroundColor.Value }); 151 | } 152 | } 153 | } 154 | } 155 | } -------------------------------------------------------------------------------- /src/Tests/Styles/WorksheetStyles.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Styles 2 | { 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using DocumentFormat.OpenXml.Packaging; 7 | using OpenSpreadsheet; 8 | using OpenSpreadsheet.Configuration; 9 | 10 | using Xunit; 11 | 12 | using OpenXml = DocumentFormat.OpenXml.Spreadsheet; 13 | 14 | public class WorksheetStyles : SpreadsheetTesterBase 15 | { 16 | private static readonly Dictionary cellValues = new Dictionary() 17 | { 18 | { 1, "The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog." }, 19 | { 2, "B" }, 20 | }; 21 | 22 | private static readonly Dictionary headerNames = new Dictionary() 23 | { 24 | { 1, "Header1" }, 25 | { 2, "Header2" }, 26 | }; 27 | 28 | [Fact] 29 | public void TestAutoFilter() 30 | { 31 | var filepath = base.ConstructTempXlsxSaveName(); 32 | using (var spreadsheet = new Spreadsheet(filepath)) 33 | { 34 | spreadsheet.WriteWorksheet("Sheet1", base.CreateRecords(10), new WorksheetStyle() { HeaderRowIndex = 1, ShouldAutoFilter = true }); 35 | spreadsheet.WriteWorksheet("Sheet2", base.CreateRecords(10), new WorksheetStyle() { HeaderRowIndex = 2, ShouldAutoFilter = true }); 36 | spreadsheet.WriteWorksheet("Sheet3", base.CreateRecords(10), new WorksheetStyle() { ShouldAutoFilter = false }); 37 | } 38 | 39 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 40 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 41 | { 42 | base.SpreadsheetValidator.Validate(spreadsheetFile); 43 | Assert.False(base.SpreadsheetValidator.HasErrors); 44 | 45 | using (var filestream = new FileStream(spreadsheetFile, FileMode.Open, FileAccess.Read, FileShare.Read)) 46 | using (var spreadsheetDocument = SpreadsheetDocument.Open(filestream, false)) 47 | { 48 | var workbookPart = spreadsheetDocument.WorkbookPart; 49 | 50 | var worksheet1 = ((WorksheetPart)workbookPart.GetPartById(workbookPart.Workbook.Descendants().First(s => "Sheet1".Equals(s.Name)).Id)).Worksheet; 51 | var autoFilter1 = worksheet1.Elements().FirstOrDefault(); 52 | Assert.Equal("A1:B1", autoFilter1.Reference); 53 | 54 | var worksheet2 = ((WorksheetPart)workbookPart.GetPartById(workbookPart.Workbook.Descendants().First(s => "Sheet2".Equals(s.Name)).Id)).Worksheet; 55 | var autoFilter2 = worksheet2.Elements().FirstOrDefault(); 56 | Assert.Equal("A2:B2", autoFilter2.Reference); 57 | 58 | var worksheet3 = ((WorksheetPart)workbookPart.GetPartById(workbookPart.Workbook.Descendants().First(s => "Sheet3".Equals(s.Name)).Id)).Worksheet; 59 | var autoFilter3 = worksheet3.Elements().FirstOrDefault(); 60 | Assert.Null(autoFilter3); 61 | } 62 | 63 | File.Delete(spreadsheetFile); 64 | } 65 | } 66 | 67 | [Fact] 68 | public void TestColumnWidths() 69 | { 70 | const double maxWidth = 20; 71 | const double minWidth = 10; 72 | 73 | var filepath = base.ConstructTempXlsxSaveName(); 74 | using (var spreadsheet = new Spreadsheet(filepath)) 75 | { 76 | spreadsheet.WriteWorksheet("Sheet1", base.CreateRecords(10), new WorksheetStyle() { MaxColumnWidth = maxWidth, MinColumnWidth = minWidth, ShouldAutoFitColumns = true }); 77 | } 78 | 79 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 80 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 81 | { 82 | base.SpreadsheetValidator.Validate(spreadsheetFile); 83 | Assert.False(base.SpreadsheetValidator.HasErrors); 84 | 85 | using (var filestream = new FileStream(spreadsheetFile, FileMode.Open, FileAccess.Read, FileShare.Read)) 86 | using (var spreadsheetDocument = SpreadsheetDocument.Open(filestream, false)) 87 | { 88 | var workbookPart = spreadsheetDocument.WorkbookPart; 89 | var worksheetPart = (WorksheetPart)workbookPart.GetPartById(workbookPart.Workbook.Descendants().First(s => "Sheet1".Equals(s.Name)).Id); 90 | var columns = worksheetPart.Worksheet.GetFirstChild(); 91 | foreach (OpenXml.Column column in columns.ChildElements) 92 | { 93 | Assert.True(column.Width >= minWidth && column.Width <= maxWidth); 94 | } 95 | } 96 | 97 | File.Delete(spreadsheetFile); 98 | } 99 | } 100 | 101 | [Fact] 102 | public void TestShouldFreezeHeaderRow() 103 | { 104 | var filepath = base.ConstructTempXlsxSaveName(); 105 | using (var spreadsheet = new Spreadsheet(filepath)) 106 | { 107 | spreadsheet.WriteWorksheet("Sheet1", base.CreateRecords(10), new WorksheetStyle() { HeaderRowIndex = 1, ShouldFreezeHeaderRow = true }); 108 | spreadsheet.WriteWorksheet("Sheet2", base.CreateRecords(10), new WorksheetStyle() { HeaderRowIndex = 4, ShouldFreezeHeaderRow = true }); 109 | spreadsheet.WriteWorksheet("Sheet3", base.CreateRecords(10), new WorksheetStyle() { ShouldFreezeHeaderRow = false }); 110 | } 111 | 112 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 113 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 114 | { 115 | base.SpreadsheetValidator.Validate(spreadsheetFile); 116 | Assert.False(base.SpreadsheetValidator.HasErrors); 117 | 118 | using (var filestream = new FileStream(spreadsheetFile, FileMode.Open, FileAccess.Read, FileShare.Read)) 119 | using (var spreadsheetDocument = SpreadsheetDocument.Open(filestream, false)) 120 | { 121 | var workbookPart = spreadsheetDocument.WorkbookPart; 122 | 123 | var sheet1 = ((WorksheetPart)workbookPart.GetPartById(workbookPart.Workbook.Descendants().First(s => "Sheet1".Equals(s.Name)).Id)).Worksheet; 124 | var pane1 = sheet1.SheetViews.FirstOrDefault()?.Descendants().FirstOrDefault(); 125 | Assert.Equal(OpenXml.PaneStateValues.Frozen, pane1.State); 126 | Assert.Equal(OpenXml.PaneValues.BottomLeft, pane1.ActivePane); 127 | Assert.Equal(1, pane1.VerticalSplit); 128 | Assert.Equal("A2", pane1.TopLeftCell); 129 | 130 | var sheet2 = ((WorksheetPart)workbookPart.GetPartById(workbookPart.Workbook.Descendants().First(s => "Sheet2".Equals(s.Name)).Id)).Worksheet; 131 | var pane2 = sheet2.SheetViews.FirstOrDefault()?.Descendants().FirstOrDefault(); 132 | Assert.Equal(OpenXml.PaneStateValues.Frozen, pane2.State); 133 | Assert.Equal(OpenXml.PaneValues.BottomLeft, pane2.ActivePane); 134 | Assert.Equal(4, pane2.VerticalSplit); 135 | Assert.Equal("A5", pane2.TopLeftCell); 136 | 137 | var sheet3 = ((WorksheetPart)workbookPart.GetPartById(workbookPart.Workbook.Descendants().First(s => "Sheet3".Equals(s.Name)).Id)).Worksheet; 138 | var pane3 = sheet3.SheetViews.FirstOrDefault()?.Descendants().FirstOrDefault(); 139 | Assert.Null(pane3); 140 | } 141 | 142 | File.Delete(spreadsheetFile); 143 | } 144 | } 145 | 146 | [Fact] 147 | public void TestShouldWriteHeader() 148 | { 149 | var filepath = base.ConstructTempXlsxSaveName(); 150 | using (var spreadsheet = new Spreadsheet(filepath)) 151 | { 152 | spreadsheet.WriteWorksheet("Sheet1", base.CreateRecords(10), new WorksheetStyle() { ShouldWriteHeaderRow = true }); 153 | spreadsheet.WriteWorksheet("Sheet2", base.CreateRecords(10), new WorksheetStyle() { ShouldWriteHeaderRow = false }); 154 | } 155 | 156 | var fileSavedByExcel = base.SaveAsExcelFile(filepath); 157 | foreach (var spreadsheetFile in new[] { filepath, fileSavedByExcel }) 158 | { 159 | base.SpreadsheetValidator.Validate(spreadsheetFile); 160 | Assert.False(base.SpreadsheetValidator.HasErrors); 161 | 162 | using (var filestream = new FileStream(spreadsheetFile, FileMode.Open, FileAccess.Read, FileShare.Read)) 163 | using (var spreadsheetDocument = SpreadsheetDocument.Open(filestream, false)) 164 | { 165 | var workbookPart = spreadsheetDocument.WorkbookPart; 166 | var sharedStringTablePart = workbookPart.GetPartsOfType().FirstOrDefault(); 167 | 168 | var sheet1 = ((WorksheetPart)workbookPart.GetPartById(workbookPart.Workbook.Descendants().First(s => "Sheet1".Equals(s.Name)).Id)).Worksheet; 169 | foreach (var kvp in headerNames) 170 | { 171 | var cell = sheet1.Descendants().ElementAt((int)kvp.Key - 1); 172 | var sharedStringValue = base.GetSharedStringValue(sharedStringTablePart, cell.CellValue.InnerText); 173 | Assert.Equal(kvp.Value, sharedStringValue); 174 | } 175 | 176 | var sheet2 = ((WorksheetPart)workbookPart.GetPartById(workbookPart.Workbook.Descendants().First(s => "Sheet2".Equals(s.Name)).Id)).Worksheet; 177 | foreach (var kvp in cellValues) 178 | { 179 | var cell = sheet2.Descendants().ElementAt((int)kvp.Key - 1); 180 | var sharedStringValue = base.GetSharedStringValue(sharedStringTablePart, cell.CellValue.InnerText); 181 | Assert.Equal(kvp.Value, sharedStringValue); 182 | } 183 | } 184 | 185 | File.Delete(spreadsheetFile); 186 | } 187 | } 188 | 189 | private class TestClass 190 | { 191 | public string LongText { get; set; } = cellValues[1]; 192 | public string ShortText { get; set; } = cellValues[2]; 193 | } 194 | 195 | private class TestClassMap : ClassMap 196 | { 197 | public TestClassMap() 198 | { 199 | base.Map(x => x.LongText).Index(1).Name(headerNames[1]); 200 | base.Map(x => x.ShortText).Index(2).Name(headerNames[2]); 201 | } 202 | } 203 | } 204 | } -------------------------------------------------------------------------------- /src/Tests/Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net472 5 | false 6 | 7.2 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | all 23 | runtime; build; native; contentfiles; analyzers 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | --------------------------------------------------------------------------------