MiniExcel icon indicating copy to clipboard operation
MiniExcel copied to clipboard

Parse public fields (not only public properties) to allow compatibility with Vb.net withtout explicitely setting Property tags on field

Open jsgervais opened this issue 2 years ago • 1 comments

Excel Type

  • [X] XLSX
  • [ ] XLSM
  • [ ] CSV
  • [ ] OTHER

Upload Excel File

How to reproduce: .net framework 4.8 project

Basic template file, two strings titles in the header, and one line rows.
mini_TotalEnVigueur.xlsx

MiniExcel Version

1.30.3, likely to affect previous versions since April 17 2021

Description

MiniExcel.SaveAsByTemplate with a dictionnary object

­> MiniExcel\src\MiniExcel\OpenXml\ExcelOpenXmlTemplate.Impl.cs
On line 859, property with s.Name = "type" is being added added twice to the dictionary

System.ArgumentException: An item with the same key has already been added.

Change introduced added April 17 2021

xRowInfo.PropsMap = xRowInfo.IEnumerableGenricType.GetProperties()
    .ToDictionary(s => s.Name, s => new PropInfo { PropertyInfo = s, UnderlyingTypePropType = Nullable.GetUnderlyingType(s.PropertyType) ?? s.PropertyType })

Here is the datamodel I send (it's in VB, and nested type are not seen from reflection, so I send it as a dictionnary instead:

Public Class EnVigueurDataModel
            Public TitreTypeAssurance As String
            Public TitreDateRapport As String


            Public Class EnVigueurLigne
                Public PoliceNumero As String

                <ExcelFormat("YYYY-mm-dd")>
                Public DateEntreeEnVigueur As Date
                Public Assure As String
                Public Sexe As String

                <ExcelFormat("YYYY-mm-dd")>
                Public DateNaissance As String
                Public Tabagisme As String
                Public Statut As String
                Public Vendeur As String

                <ExcelFormat("YYYY-mm-dd")>
                Public DateChangementStatut As String
                <ExcelFormat("# ##0.00 $")>
                Public CapitalAssure As String 'montantCouverture
                <ExcelFormat("# ##0.00 $")>
                Public Prime As Decimal
                <ExcelFormat("# ##0.00 $")>
                Public CapitalAssureDa As Decimal = 0.0
                <ExcelFormat("# ##0.00 $")>
                Public PrimeDa As Decimal = 0.0
                <ExcelFormat("# ##0.00 $")>
                Public PrimeTotale As Decimal

            End Class

            Public Lignes As IEnumerable(Of EnVigueurLigne)
        End Class
        MiniExcel.SaveAsByTemplate(nomFichierDestination, templatePath, DataModel.ToDictionary(Of Object))

ToDictionnary is an extension method (with newtonsoft json) to serialise unserialize object as a dictionnary :

 Public Module DictionaryExtensions
        <Extension()>
        Public Function ToDictionary(Of TValue)(obj As Object) As Dictionary(Of String, TValue)
            Dim json = JsonConvert.SerializeObject(obj)
            Dim dictionary = JsonConvert.DeserializeObject(Of Dictionary(Of String, TValue))(json)
            Return dictionary
        End Function
    End Module

jsgervais avatar May 05 '23 17:05 jsgervais

update, I removed the toDictionary() on my model and used poco reflection method after debuging the library. turns out class attributes in vb.net are fields and not automagically properties. I've been using C# for so long I forgot the compilier handles it for you.

To fix my issue, I needed to declare fields as Property for the library to map them through reflection (type.GetProperties),

i.e. Public Property PoliceNumero As String

maybe the library should consider adding type.GetFields to map data types (there's 6-7 areas in the code to fix) including this part in MiniExcel\src\MiniExcel\OpenXml\ExcelOpenXmlTemplate.cs

(...)
 public void SaveAsByTemplateImpl(Stream templateStream, object value)
        {
            //only support xlsx         
            Dictionary<string, object> values = null;
            if (value is Dictionary<string, object>)
            {
                values = value as Dictionary<string, object>;
                foreach (var key in values.Keys)
                {
                    var v = values[key];
                    if (v is IDataReader)
                    {
                        values[key] = TypeHelper.ConvertToEnumerableDictionary(v as IDataReader).ToList();
                    }
                }
            }
            else
            {
                var type = value.GetType();
                var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
                values = new Dictionary<string, object>();
                foreach (var p in props)
                {
                    values.Add(p.Name, p.GetValue(value));
                }

                var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance);
                foreach (var f in fields)
                {
                    if (!values.ContainsKey(f.Name))
                    { values.Add(f.Name, f.GetValue(value)); }
                }
            }

jsgervais avatar May 05 '23 19:05 jsgervais