From 095da360e0544f1f08488863967f43a9add59925 Mon Sep 17 00:00:00 2001 From: Mitchel Sellers Date: Fri, 30 Dec 2022 16:45:18 -0600 Subject: [PATCH 1/2] Updated to support better usage of [Display(Name="")] Attribute as well as the existing options. #37 --- .../SampleExportRecord.cs | 3 ++- .../TypeDiscovererTests.cs | 18 ++++++++++++++++++ .../TypeDiscoverer.cs | 6 ++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/NetCore.Utilities.Spreadsheet.Tests/SampleExportRecord.cs b/src/NetCore.Utilities.Spreadsheet.Tests/SampleExportRecord.cs index 21e0c51..8531ad3 100644 --- a/src/NetCore.Utilities.Spreadsheet.Tests/SampleExportRecord.cs +++ b/src/NetCore.Utilities.Spreadsheet.Tests/SampleExportRecord.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using System.ComponentModel.DataAnnotations; namespace ICG.NetCore.Utilities.Spreadsheet.Tests; @@ -99,7 +100,7 @@ public class DifferentTestExportRecord [SpreadsheetImportColumn(1)] public int Id { get; set; } - [SpreadsheetColumn(displayName: "Company")] + [Display(Name = "Testing This Out")] [SpreadsheetImportColumn(2)] public string Company { get; set; } = ""; diff --git a/src/NetCore.Utilities.Spreadsheet.Tests/TypeDiscovererTests.cs b/src/NetCore.Utilities.Spreadsheet.Tests/TypeDiscovererTests.cs index b8089cf..e7d91c1 100644 --- a/src/NetCore.Utilities.Spreadsheet.Tests/TypeDiscovererTests.cs +++ b/src/NetCore.Utilities.Spreadsheet.Tests/TypeDiscovererTests.cs @@ -1,5 +1,7 @@ using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Linq; +using Bogus.DataSets; using FluentAssertions; using Xunit; @@ -39,6 +41,16 @@ public void Sets_DisplayName_From_SpreadsheetColumn_Attribute() results.First().DisplayName.Should().Be("Some Prop Name"); } + [Fact] + public void Sets_DisplayName_From_Display_Attribute() + { + var results = TypeDiscoverer.GetProps(typeof(Sets_DisplayName_From_DisplayAttribute_Attribute_TestCase)); + + results.Should().HaveCount(1); + + results.First().DisplayName.Should().Be("Some Prop Name"); + } + [Fact] public void Property_Excluded_From_SpreadsheetIgnore_Attribute() { @@ -81,6 +93,12 @@ private class Sets_DisplayName_From_SpreadsheetColumn_Attribute_TestCase public string SomeProp { get; set; } } + private class Sets_DisplayName_From_DisplayAttribute_Attribute_TestCase + { + [Display(Name = "Some Prop Name")] + public string SomeProp { get; set; } + } + private class Property_Excluded_From_SpreadsheetColumn_Attribute_TestCase { [SpreadsheetColumn(ignore: true)] diff --git a/src/NetCore.Utilities.Spreadsheet/TypeDiscoverer.cs b/src/NetCore.Utilities.Spreadsheet/TypeDiscoverer.cs index 87d5a08..0193fba 100644 --- a/src/NetCore.Utilities.Spreadsheet/TypeDiscoverer.cs +++ b/src/NetCore.Utilities.Spreadsheet/TypeDiscoverer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Text.RegularExpressions; namespace ICG.NetCore.Utilities.Spreadsheet; @@ -48,6 +49,11 @@ public static IList GetProps(Type t) propName = sca.DisplayName ?? propName; width = sca.Width; } + else if (attr is DisplayAttribute display) + { + if (!string.IsNullOrEmpty(display.Name)) + propName = display.Name; + } } if (ignored) continue; From 34da96d1fab2e42f2eb8c22d26feb8b8a4027f11 Mon Sep 17 00:00:00 2001 From: Mitchel Sellers Date: Fri, 30 Dec 2022 16:58:13 -0600 Subject: [PATCH 2/2] Addresses import by sheet name #26 --- .../OpenXmlSpreadsheetParserTests.cs | 19 ++++++++++ .../SampleFiles/ImportSample.xlsx | Bin 8729 -> 9633 bytes .../ISpreadsheetParser.cs | 10 ++++++ .../OpenXmlSpreadsheetParser.cs | 34 +++++++++++++++--- 4 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/NetCore.Utilities.Spreadsheet.Tests/OpenXmlSpreadsheetParserTests.cs b/src/NetCore.Utilities.Spreadsheet.Tests/OpenXmlSpreadsheetParserTests.cs index e9aff96..f245209 100644 --- a/src/NetCore.Utilities.Spreadsheet.Tests/OpenXmlSpreadsheetParserTests.cs +++ b/src/NetCore.Utilities.Spreadsheet.Tests/OpenXmlSpreadsheetParserTests.cs @@ -42,4 +42,23 @@ public void ParseDocument_ShouldReturnProperData() Assert.Equal("John Smith", firstRecord.Name); Assert.Equal(55, firstRecord.Age); } + + [Fact] + public void ParseDocument_ShouldReturnProperData_BySheetName() + { + //Arrange + var filePath = "../../../SampleFiles/ImportSample.xlsx"; + var expectedCount = 3; + + //Act + var result = _spreadsheetParser.ParseDocument(File.OpenRead(filePath), "Test Sheet", true); + + //Assert + Assert.Equal(expectedCount, result.Count); + var firstRecord = result.First(); + Assert.Equal("John Smith", firstRecord.Name); + Assert.Equal(55, firstRecord.Age); + var lastRecord = result.Last(); + Assert.Equal("Adam", lastRecord.Name); + } } \ No newline at end of file diff --git a/src/NetCore.Utilities.Spreadsheet.Tests/SampleFiles/ImportSample.xlsx b/src/NetCore.Utilities.Spreadsheet.Tests/SampleFiles/ImportSample.xlsx index c2244ba41b02bd8dbc3753f2bae1736f568f0677..2a5569998e5c2ebc2b3610f63861aea7455b769e 100644 GIT binary patch delta 4300 zcmbtXXIK-;whe?DN9=zcAa&ytEcmp=NNy6N@mljUs;}l^5-Vev0)u2C0%Y%i<(r80uP!r`B{wsp_{HIC^ zB{_X#8uffUf0Eay0p)<$3hzrp#XtaHh3E}1(IJ5zq5)`c)EzV$bv4k_qsr74{Y(}T zYVURdTq+Dd1{LGYdB{A=8!Gc=58%eD4{lYFI(v?nRsO)fbzHBPd1l%WP}-IJeP?lS zFn4ijruk`-Ob5Bs_g*z%kCc{mO?rbU-`>fMM&5AtQQ)yL4=L>{B&Ey+>W!vpcxGPgKCNagW11-o(d zj9h#DMkAtjZ5vCs7z1UWox?3`tQgUW$TE2dQUW~DT17`GP_7ark3|t-;l`EuwUwkw z=nu=C?S!fh1+zea>M**LCpGJn4(YoSUaOk*wY+RLT`OEnk}ut?U*l_|+)DjY!c7BR zgtnYSOwC=PUAM?X+v#ymgE`o4s+;|f)sUW~bYYDB09KAFh!UL15M9e9pbH_q&U)Br zkxq?*Z`|9Q*m5^;L?S3aGC%s}6;{bWG(XAPI${&ZloI5Hg-X}=dk53I6~5$Hyi{XN z#Hom8h6^%8mKs}A*$I2a6$m73-A|Y*VW;$oO|}@kH~#TTOip}vTEmVT&l#oP^3ABh z=p4_}Y3%96wXlWDNPkcpZZ`bFeCpa=O!PdgqPW1@de=NvYDM7{IdXJdQ*3y45MwK3 z@TPsY$Ie;(>9UpGM#@=lLTQ}LDar7*m>~NPr&pN?4=(2_x-PG5P`n8EqgOlF>auHy z>`Y4entk-#6nM49JrSHy)um$L?pDR^W?jd!aTOcC+%EO`5Vg|jU5?~-6BH7Qpl_N# zNf^xr36w(o^?_{ZZE130(U(>upT#B*f&lmhkdzl_#K3ncJBmt8mT zdBve!h5>gSDqTuHno8ZWuZpV{n?T%8$C8@jqdA=P_EXa3CLu$Xj$ z`~x;OPs%qDDr3faSL8+12T84h2dcdR1l2zdL|iHvq213}jvt})PX`MrI^Cr8fte1( z{BfRQP$eWX^=6iN0OTXL{yH$qd`ZC)ZxPPxN9D0_Ur(A&Ipnh8PoE5H#rR7?`l|Dw zx>*_+InFJWyBBeoZT~GYts2cFe1C(XQbBpc?=c~yrDfj6OS&QV%%rt16u;E=<_sMz zx!uO^!nqE1;WY36)_a z(b6kYd*Xx6CvrcRF7f$&78Iz1k~L4~55EH09U{#SVX?0yn-A$(618fouZl{KOri{s zcUzQspUN6L!loD_?7vk5&aKAet1kf>v(t$w5c012+07KR;vmz*yKp;)+I-WItkFAkyNBC~`$mHw!d?2hLB4@8al5(+UhB)ms?&!%#B&Gy zIh$dU9E-6vCW!^j-*hC_I=DwbO6pr5=mT&>Mn(}C$%&QtEw2Ud2jXYMm?;)QOSWJy16ELZ zIkKI4559cULEl$bOjh^Bz*jZCv>Z8cS2o|Uq(1hObZRv-sPidZ!_{o8<4WaP_2(@~?@i9ur#Yhicpiyh%# zVGZ`eQ@jR3w!&qWHLYr;k}LFl+&0t&=J=vLtpy!CyX_*W*}5W@NDjB;r%s;XyZH*% zyc)OoZu=2giQpLK(}`nU-TY+HBLmu0gv+t5tZeJ-3_llNKDABH>aax5h^ge-9)#GR z7YuQ*$$V)$kIA{>LQn54+b)ARo6JhbE;*N@Ee=#Xf;*$ws$Whb2!Sv)(+GfSbvo(-i@ zShbliwkU*=H+?51JeE38ohq1EAeR=PK)zQwBNjt{(KKUh4AcQ5I?0@Y8@6sl9nBQ~ zCN^jEWBet{&F}I)i#I{jg}*M2`o@;75k8!(6kbAurTuNZqXU#+)m950S<^8GyWjYt zWy_LfD8*){YJ8CF7 zn8;^l0PY?SE7l|BT|B8cL~X0%Ty^6l&r-!mo0m6mdqMpyTx{HcRNKYBS#$R7JOjAn zd5`)2S3_r?5$(B}b&KAPiI=2NCfWSPs73G3*sChFf$kxQhpb_#x0e<9qb)zYhBrXe znqPggp&WcvQLf6FkNQ%d&2VDoGf|3`h3Oaso0-xShRR+s^yAPHu2QCBeoHy45JW$G z&sSr1%tgwoU<@4QJab_0D-iKPP?H+Fs;c)TwB9o$Y%2K1N*KxSk5RilWn4xQ|Ke0{ z&j};JhNK(mc8W1a&68iK_TA5yo+HqDQX=kq!>S%1v8K0(OyYUs{7(+?-_7Kixp6)} zvVE@hne{Y-+S)Cqo`n&_eit9Xu|bavVCMoB{GMa-X3i008lU-8TvlCXJj`X)8m~4V#HlZbh%w8uq+1`*Wdcw67XWJCACvMH0%t^ zOU=ZH$7oX(inv`bf{2Dc9tk}Yk4FRkH1Fh1j_DMldBy5yphWZHR8X^Gkf^PwbIQ{D zW}iK&ozhr3_~9{GSF3Q3OoLik)8M7KIdkW~l0BCK^Hob4bAwXWk$jeO1y)7-siWP7 zvHJ)%4l*tww&bWl;kS073>@jynLFajJj3Bj8raj~13~}PUJ_?0Lk)z^s4-&V65#WM9XXU)T$#6W@2dT=Tn@+*r;%Zc4hhA9)nzMa&)Nn zY8jSC#Uh`+mCxrTNh0xTA7A#FIbBnJHKa4tuw;LD0w|pIsf(Z zYC|El3+vj~e43G?$;_Q$NUPV0_Y?XgZNnY%p+<4=2cYQnW1hPcIrkoZ{kUhx{+TfT z6S%RzkGK%Z*n1`Ftkb68cSiXyH6kGuAqMh)_ujZdZX=RH+!ps`uK(Q;62t$m(SZ|y z@pApU*aQGL{{{WIrr?}lB3yqLUc`o;{U3NAt{NsJ@wW{A7w${UxqtPaKCVtqzCNOV zE)uwRJil`iC#eD>P7KU~qvk=-wsQQr*T*-=!wDC`!vg(hs`}4+6QIPz#Si^E_K6bA p@Siv!_!6#;hY$KUdx`BZ5&N}O?z0V(K@BMw>K1C)kbgx*L0I47VY5*+&0N?|- zea-iFrT_ras9s;;qeSB}hRk}TzM<+%fArfjE%dd}W6`%Olm}+J7R_wunUvmF zPJma&StfmbL{ZKME8<2}$^57}wovCX3%*-cqHeI3pyOW&H)Z=E{oK;-bq32Hz;X zGv9URj0|`+PP0m-cXSRpWZG;!c&7~>e8(pkmAJb&uz3TuGq&dN;OlkVu3?n7<#+#sOKP0*==Bct%6G(Y1cbxAhv| zPUWPn4aVPvzuNI`lFd7ue~$Ik*-tKvgQS6hGSR9`;gG(t^Tn^eyhta-#(mv*cso~a z*u$F-nn05FCsGQXqrqY_43&ls={e2l5I=P*pJ=ty@6&@x$^EDi9tME-+6*pZi+MIn zPY=p(&|V^bE6Y(%w+p5^Q_qr-QD1B=-?+;QnPG6PZqYI%NMDI;frEy_#uXcEpkOUT zam-uXt*iIFO#|s2bqMYY_8_E6%o{Ixt5INukDOl6*;tU)R|G98HOgIl@iO|IhQ5?i z8|EEpsUm?j&g8ANKunWjsRrTcv()`u2u@>tQAu!=jr4ev@7a}(XJSoDgkh#8ue_BNbZ&iTjl%OCOYGV+ z*!HI~DeO*k#nlW z#6uNHUZYg;eUwpJIH`1gzMtQ7D&BACsVQ6?bn7;0RVs|czm;x$Q^b{>oP~|MsIrx5 zD_aso!tSxbAjx7_;XhVR=G{X#K8$n1gf7c|43Qt^>k8U8-dJ_FJEcKaVzj@ECQt>$ z&8c>p3lpxT=jYXCN-h3UZHq}C8DN22fm;WhoO52gavHX;!}BFJ_jW*@>(E8Vg(SDD z_1X_%S5qvXzxpisNSn&2%cvGx?Jc)HLu&Z2kSj45;ngLwX&+U~@w&TSPfWp-~ooc98xWsHg#{`PF7)2D?>d=D1Jw>g5s+4Qk z&+o00YntcGi`!=0yh8_po@EnYd%lt7fz;9BF(VrNLowqXJ^IOn&)$jaFE)qRds1t? z@}0ui9;@r*W$P5qv`y2&E{|Q0;NWym$-8A>I=!Whb*0Xid6}q&4q+>v!H+?VSG4?b z1HoK~Wsi|b#`4c{Kkh}55(R&0)JzmPr4uh3<8hBHg0+$ij%tWw9>#(qJ}lsGQKkw% zN+2zo<3oV~;Y#|pIn)JfUM<@?+E=YKG}5M)BsIcc=pBAj5W*(vPphfpSk%^MiKpv9 z_}A4FxvxwDWb%KS?9QIAD?QjiM3e?cqe@p+w}PU>OZR$1&Q>INYGTnTpSZC_{aQMQ zH3{XNWL7*s2Lk@im$B*@*#~6cwo>BD=|m{dIC9+)q`(~)&l08X=Z3N}eGZZ2-?7}mnB6Uy3H+T%q z_1@HeeyCwJ=J=u2PIX*Eb2D#_sOQ>wi_YvJbzxjM&-elR1tV34aKQM+B%jz(m);@m z_x9hOrr{%qu99Q=HO?v%ZmhgoWI_mU;(!4US~NX)t@uTo&E8Uo_Q`wE&e`wRhe&&# ztAqVh3*>kIBO>%<3E2nrl*&_*#RRiR&x_75L6%5?WA?;$(RGoG)RaY9nwc54vpvRD zQA93TL0bxZ)4@mIb5uWEr>Y^b0>;m-8o7|-#oMOV!0r^kLZ`IMp5EE;-~VORWOqF12e_`K?G@Dey2+ef)(t4GSg=*plYe_N3+>Q8 ziSUeIXy@Seg|Z!=r8GK*l5`D!$kW?Q!!#=iP4OcIyc`?>(rEm%4&{m4febZk$3C;> z4}DR(IJm%WU<~YI5-E@ho(i!O{#jHaU*s=rpp>Fz{2{H;}?9ub)EqT;OJiSXJJc(+H+MPw1?aeg)1< zq{0vK8x;@ow|BbC_cD4gXP$dMvthomgXQPg%9Dr-Q7*#`B>eg%?uvA-e!N!2GZs@9 z^CX5@u|TY>z;RJv{`)iU1c1S6_j|56t%=o;$C)`A4}jSscS@Fi9Q&crSE!G3g;^&f zXJ__&&q1pZ?v~JS*ObgwOQS0H3;K&G#k0xF(g*1d&hXp27Y?B;@@n^y=4q`!^v^+l z7KzZ2I0R+l@Q)J}`sSH1UE1=kn8J&yddO77RF$2*B}qLRY;IefLHLJ>Ii?* zl|(5G*4>Lc2Orn4>%r~QAJKBFh6SF+NKKeg8hPb?WjaL}ut`Ql)?#0NSTDe|x{kh1 zi|=ixjJM7fh9Q=IHy579xb06KS<{NErn#=XV(Gc=JICxmi{>ZResZMwQ1GQphXZ4> ziO*&_u??OsK`QYYn?=~$Ao8OGE$*=!UdL%NKx%qLmZM(<|KHTeXeo)}2i+3$~ zeGHygj*Xynq(c2@l3E3oLcoftHt5O35~7>VXBr~q$V5m2YuNkB@;AUfPr+h=bUc%f zdi4%Po@?4mO;jRYut&q0(Y8f{Cc{!=`$f@E&N*nLBc)V5&0Z~B_mk%?;W{6JF ztf2G@jr-D?LQi6r^<5%ixtuKUpcs$=6;9VA!xmQTUnKkHj&j{c!W{du!k!1(kHc$8 z;g9S~l!I0E?OAtuY2_!gHz6Xg2sY+C4AmuEN8bYP-=xP)HD|Qr6OOWu%kj!?eicX2 zb&HvK$w`|{cUO80x7;I9001o2Z((g1k*OCR4IO4lXM zz%K*9GhYSai5r9=<^2JveKV!mSfonuh@$fg^W&>1t~Pl0vv{i?J@H>>{R< zpN;OXuLov<-;yF5BQ3xU{bz$rPU*i!JH}UlpZhQU1pqkyZTkN`lnwyERMT-_2m)Z} ze>4dM09^T-TP0>sKn!z7kb@G!h=~-`pv16YIs|z*|4Bf9U2|@{vEo1nE?Pp y4vdPBfbM?^jojaZ|Jx2AZ~_4TM!KZy-MfF1nS}|6`cEf1@{A#~!2_N@PyYuvekt7m diff --git a/src/NetCore.Utilities.Spreadsheet/ISpreadsheetParser.cs b/src/NetCore.Utilities.Spreadsheet/ISpreadsheetParser.cs index 7d698b0..b410492 100644 --- a/src/NetCore.Utilities.Spreadsheet/ISpreadsheetParser.cs +++ b/src/NetCore.Utilities.Spreadsheet/ISpreadsheetParser.cs @@ -36,4 +36,14 @@ public interface ISpreadsheetParser /// If set to true will skip the first row of data as header information /// The parsed information List ParseDocument(Stream fileStream, int worksheetNumber, bool skipHeaderRow) where T : new(); + + /// + /// Parses the provided document and returns a list of T objects based on the input data, using the specific worksheet by name + /// + /// The type to use for importing + /// The contents of the Excel File (XLSX format + /// + /// If set to true will skip the first row of data as header information + /// + List ParseDocument(Stream fileStream, string worksheetName, bool skipHeaderRow) where T : new(); } \ No newline at end of file diff --git a/src/NetCore.Utilities.Spreadsheet/OpenXmlSpreadsheetParser.cs b/src/NetCore.Utilities.Spreadsheet/OpenXmlSpreadsheetParser.cs index d8ed1fa..759bc26 100644 --- a/src/NetCore.Utilities.Spreadsheet/OpenXmlSpreadsheetParser.cs +++ b/src/NetCore.Utilities.Spreadsheet/OpenXmlSpreadsheetParser.cs @@ -29,6 +29,18 @@ public class OpenXmlSpreadsheetParser : ISpreadsheetParser /// public List ParseDocument(Stream fileStream, int worksheetNumber, bool skipHeaderRow) where T : new() + { + return ParseDocumentInternal(fileStream, worksheetNumber, string.Empty, skipHeaderRow); + } + + /// + public List ParseDocument(Stream fileStream, string worksheetName, bool skipHeaderRow) where T : new() + { + return ParseDocumentInternal(fileStream, null, worksheetName, skipHeaderRow); + } + + private List ParseDocumentInternal(Stream fileStream, int? worksheetNumber, string worksheetName, + bool skipHeaderRow) where T : new() { //Validate object is properly created var importColumnDefinitions = typeof(T) @@ -49,13 +61,24 @@ public class OpenXmlSpreadsheetParser : ISpreadsheetParser var workbookPart = excelDoc.WorkbookPart; if (workbookPart == null) throw new SpreadsheetParserException("Spreadsheet has no WorkbookPart"); - var sheet = workbookPart.Workbook.Descendants().Skip(worksheetNumber - 1).FirstOrDefault(); - if (sheet == null) throw new SpreadsheetParserException($"Workbook does not have {worksheetNumber} sheets"); + Sheet sheet; + if (worksheetNumber.HasValue) + { + sheet = workbookPart.Workbook.Descendants().Skip(worksheetNumber.Value - 1).FirstOrDefault(); + if (sheet == null) throw new SpreadsheetParserException($"Workbook does not have {worksheetNumber} sheets"); + } + else + { + sheet = workbookPart.Workbook.Descendants().First(s => s.Name == worksheetName); + if (sheet == null) + throw new SpreadsheetParserException($"Workbook does not have a sheet named '{worksheetName}'"); + } + if (sheet.Id == null || !sheet.Id.HasValue || sheet.Id.Value == null) throw new SpreadsheetParserException($"Sheet {worksheetNumber} has a null Id"); - if (workbookPart.GetPartById(sheet.Id.Value) is not WorksheetPart wsPart) + if (workbookPart.GetPartById(sheet.Id.Value) is not WorksheetPart wsPart) throw new SpreadsheetParserException($"Sheet {worksheetNumber} with Id {sheet.Id.Value} is not in the workbook"); - + var collection = new Collection(); var skipRows = skipHeaderRow ? 1 : 0; var expectedColumns = importColumnDefinitions.Max(c => c.Column) - 1; @@ -86,8 +109,9 @@ public class OpenXmlSpreadsheetParser : ISpreadsheetParser return collection.ToList(); - } + + private static bool IsOfType(Type t) { var typeToCheck = typeof(T);