.Net Core Serializing File and Objects

For one of my API methods I wanted to send a file as well as object data. This is straight-forward enough when the object data consists of value types: the front end adds key-value-pairs to a FormData object, including the File object as one of the values; and the .NET Core back-end model object includes an IFormFile. e.g.

// JavaScript client
let data = new FormData();       
data.append("file", file);
data.append("id", "44b6...");
return this.httpClient.fetch(`...`, { method: 'post', body: data });
// C# Model
public class MyObj {
    public Microsoft.AspNetCore.Http.IFormFile File { get; set; }
    public Guid Id { get; set; }
}
// C# Controller Method
[HttpPost]
public async Task<IActionResult> Post(MyObj request) { ... }

However this approach fails if the model includes objects as in the following case where Numbers will be null.

public class MyObj {
    public Microsoft.AspNetCore.Http.IFormFile File { get; set; }
    public Guid Id { get; set; }
    public List<int> Numbers { get; set; }
}

At this point the model deserialization in .NET Core and the serialization done in JavaScript don’t match. However I found trying to use the suggested techniques to be somewhat over-complicated. My impression is the ‘right’ approach is to use a custom Model Binder. This seemed nice enough, but then got into details of needing to create and configure value binders, when I really just wanted to use some built-in ones for handling lists.

In the end I went with a different, perhaps less flexible or DRY, but vastly simpler approach: creating objects that shadowed the real object and whose get/set did the serialization.

public class ControllerMyObj : MyObj {
    public new string Numbers {
        get {
            return base.Numbers == null ? null : Newtonsoft.Json.JsonConvert.SerializeObject(base.Numbers);
        }
        set {
            base.Numbers = Newtonsoft.Json.JsonConvert.DeserializeObject<List<int>>(Numbers);
        }
    }
}

// Controller Method
[HttpPost]
public async Task<IActionResult> Post(ControllerMyObj request) { 
   MyObj myObj = request;
   ...
}

And now the front-end needs to be changed to send JSON serialized objects. That can be done specifically by key or using a more generic approach as follows.

let body = new FormData();
Object.keys(data).forEach(key => {
    let value = data[key];
    if (typeof (value) === 'object')
        body.append(key, JSON.stringify(value));
    else
        body.append(key, value);
});
body.append("file", file);
// fetch ...

.NET Core Code Coverage

I haven’t written anything for a while because, frankly, I’m passed the platform R&D stages of my current application and just churning out features, and so far I haven’t found anything much to inspire me to write about. After digging through my code looking at things I’d done, one area I thought may be interesting to readers is getting (free) code coverage in .NET Core.

OpenCover

The open source tool of choice for code coverage seems to be OpenCover. This comes as a nice zip file which can be extracted anywhere. Getting set up for .NET Core was mostly a case of following various instructions online, and there was just one gotcha: the MSBuild DebugType must be full, which is typically not the case for .NET Core where the goal is deployment to multiple operating systems. To get around this my coverage script overwrites the .proj file before running and puts portable back when it is done.

The script runs the dotnet executable from the assembly folder, meaning the assemblies aren’t directly specified in the script. The graphical output of the coverage is put together using ReportGenerator, which I have deployed inside my report output folder.

Here is a cut-down version of my Powershell script:

Push-Location

# change portable to full for projects
$csprojFiles = gci [Repository-Path] -Recurse -Include *.csproj
$csprojFiles | %{
     (Get-Content $_ | ForEach  { $_ -replace 'portable', 'full' }) | 
     Set-Content $_
}

# Setup filter to exclude classes with no methods
$domainNsToInclude = @("MyNamespace.Auth.*", "MyNamespace.Data.*")
# Combine [assembly] with namespaces
$domainFilter = '+[AssemblyPrefix.Domain]' + ($domainNsToInclude -join ' +[AssemblyPrefix.Domain]')
$filter = "+[AssemblyPrefix.Api]* $domainFilter"

# Integration Test Project
$integrationOutput = "[output-path]\Integration.xml"
cd "D:\Code\RepositoryRoot\test\integration"
dotnet build
[open-cover-path]\OpenCover.Console.exe `
    -register:user `
    -oldStyle `
    "-target:C:\Program Files\dotnet\dotnet.exe" `
    "-targetargs:test" `
    "-filter:$filter" `
    "-output:$integrationOutput" `
    -skipautoprops

# Generate Report
$reportFolder = "[output-path]\ReportFolder"
[report-generator-path]\ReportGenerator.exe `
    "-reports:$integrationOutput" `
    "-targetdir:$reportFolder"

# restore portable in projects
$csprojFiles | %{
     (Get-Content $_ | ForEach  { $_ -replace 'full', 'portable' }) |
     Set-Content $_
}

Pop-Location

The end result after opening the index.html is something like this (looks like I need to work on that branch coverage!):
coverage