Search code examples
c#asp.net-mvclinqexport-to-excelepplus

Export to Excel - LINQ - Include ForeignKey values in .ToList()?


I am attempting to add Excel Export functionality to my MVC5/Code-First Entity Framework application via the EPPlus library. On my Export View I have filled a MultiSelectList with each of my INV_Assets model properties. These then get passed into my ExportController to designate which fields of the model are to be exported.

I've gotten the Headers (via the MultiSelectList) to load into Row1 of Excel and the data from my INV_Assets model to export to excel via EPPlus LoadFromCollection() as below (example):

Code:

    [HttpPost]
    public ActionResult ExportUsingEPPlus(ExportAssetsViewModel model)
    {
        ExcelPackage package = new ExcelPackage();
        var ws = package.Workbook.Worksheets.Add("TestExport");

        var exportFields = new List<string>();
        foreach (var selectedField in model.SelectedFields)
        {
            // Adds selected fields to [exportFields] List<string>
            exportFields.Add(model.ListOfExportFields.First(s => s.Key == selectedField).Value);
        }

       {
            ws.Cells[1, i + 1].Value = exportFields[i].ToString();
        }

        var membersToShow = typeof(INV_Assets).GetMembers().Where(p => exportFields.Contains(p.Name)).ToArray();

        ws.Cells["A2"].LoadFromCollection(_db.INV_Assets.ToList(), false, TableStyles.None, BindingFlags.Default, membersToShow);

        var memoryStream = new MemoryStream();
        package.SaveAs(memoryStream);

        string fileName = "Exported-InventoryAssets-" + DateTime.Now + ".xlsx";
        string contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";

        memoryStream.Position = 0;
        return File(memoryStream, contentType, fileName);
    }

Output:

Excel Output

The only issue I still have is the ForeignKey fields within my model. For example, [Status] is defined in my INV_Assets Model as:

    [Required]
    public int Status_Id { get; set; }
    [ForeignKey("Status_Id")]
    public virtual INV_Statuses Status { get; set; }

When the current export executes though, instead of getting say AVAILABLE/RECYCLED, the [Status] column in excel contains "InventoryTracker.Models.INV_Statuses" in every cell for each record exported.

Can anyone offer insight into how to get not only the direct INV_Assets fields exported, but the ForeignKey values for Model, Location, Status, Vendor, Manufacturer, and Type also into Excel?

I did some reading and thought the Include() method would work, but no luck. (Ex.) [Type] still displays InventoryTracker.Models.INV_Types in all record cells using the following code:

ws.Cells["A2"].LoadFromCollection(_db.INV_Assets.Include("Model").Include("Manufacturer").Include("Type").Include("Status").Include("Vendor").ToList(), false, TableStyles.None, BindingFlags.Default, membersToShow);

EDIT:

I added using System.Data.Entity to my ExportController which allowed the following to compile:

ws.Cells["A2"].LoadFromCollection(_db.INV_Assets.Include(m => m.Model.model_description).Include(m => m.Manufacturer.manufacturer_description).Include(m => m.Type.type_description).Include(m => m.Status.status_description).Include(m => m.Vendor.vendor_name).ToList(), false, TableStyles.None, BindingFlags.Default, membersToShow);

I thought this would do the trick, but during execution I receive:

InvalidOperationException was unhandled by user code.

An exception of type 'System.InvalidOPerationException' occurred in EntityFramework.SqlServer.dll but was not handled in user code.

Additional Information: A specified Include path is not valid. The EntityType 'InventoryTracker.DAL.INV_Models' does not declare a naviation property with the name 'model_description'.

I don't understand why this is flagging in this manner. INV_Assets has the relation to INV_Models defined thus:

    public int Model_Id { get; set; }
    [ForeignKey("Model_Id")]
    public virtual INV_Models Model { get; set; }

And INV_Models defined as:

public class INV_Models
{
    public int Id { get; set; }

    [Required(ErrorMessage = "Please enter a Model Description.")]
    public string model_description { get; set; }

    [Required]
    [DisplayFormat(DataFormatString = "{0:MM/dd/yyyy}")]
    public DateTime created_date { get; set; }

    [Required]
    public string created_by { get; set; }

    [DisplayFormat(DataFormatString = "{0:MM/dd/yyyy}")]
    public DateTime? modified_date { get; set; }

    public string modified_by { get; set; }
}

Can anyone spot what I'm doing incorrect on the Include()?


EDIT2:

Thanks to Eric J's suggestion, the issue is solved! Added a ToString() override on each of my desired Models to ensure the value came through instead of just the object type:

public class INV_Models
{

    public override string ToString()
    {
        return this.model_description;
    }
}

public class INV_Manufacturers
{
    public override string ToString()
    {
        return this.manufacturer_description;
    }
}

public class INV_Locations
{
    public override string ToString()
    {
        return this.location_dept + "|" + this.location_room;
    }
}

public class INV_Vendors
{
    public override string ToString()
    {
        return this.vendor_name;
    }
}

public class INV_Types
{
    public override string ToString()
    {
        return this.type_description;
    }
}

public class INV_Statuses
{
    public override string ToString()
    {
        return this.status_description;
    }
}

ExportController:

    [HttpPost]
    public ActionResult ExportUsingEPPlus(ExportAssetsViewModel model)
    {
        ExcelPackage package = new ExcelPackage();
        var ws = package.Workbook.Worksheets.Add("TestExport");

        var exportFields = new List<string>();
        foreach (var selectedField in model.SelectedFields)
        {
            // Adds selected fields to [exportFields] List<string>
            exportFields.Add(model.ListOfExportFields.First(s => s.Key == selectedField).Value);
        }

        // Loops to insert column headings into Row 1 of Excel
        for (int i = 0; i < exportFields.Count(); i++)
        {
            ws.Cells[1, i + 1].Value = exportFields[i].ToString();
        }

        var membersToShow = typeof(INV_Assets).GetMembers().Where(p => exportFields.Contains(p.Name)).ToArray();

        ws.Cells["A2"].LoadFromCollection(_db.INV_Assets.ToList(), false, TableStyles.None, BindingFlags.Default, membersToShow);



        var membersToShow = typeof(INV_Assets).GetMembers().Where(p => exportFields.Contains(p.Name)).ToArray();

        var memoryStream = new MemoryStream();
        package.SaveAs(memoryStream);

        string fileName = "Exported-InventoryAssets-" + DateTime.Now + ".xlsx";
        string contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";

        memoryStream.Position = 0;
        return File(memoryStream, contentType, fileName);
    }

And the output Excel file:

Excel3


Solution

  • When the current export executes though, instead of getting say AVAILABLE/RECYCLED, the [Status] column in excel contains "InventoryTracker.Models.INV_Statuses" in every cell for each record exported.

    That is what you get if System.Object.ToString() is called on an instance of INV_Statuses.

    Try overriding ToString() in that class:

    public class INV_Statuses
    {
        public override string ToString()
        {
            // TODO: Return whatever you would like to have appear in Excel
        }
    
        // Rest of the class here
    }