Model Binding an Array from a Form Post | Asp.Net MVC & Asp.Net Core

if you form post html form fields:

<li>
  <label for="name">Your name<span>*</span></label>
  <input type="text" id="name" name="Name" />
</li>
<li>
  <label for="phoneNumber">Telephone<span>*</span></label>
  <input type="tel" id="phoneNumber" name="PhoneNumber" />
</li>

to a Controller using the [FromForm] attribute, AspNet will modelbind the form fields to your parameters by matching the name= attribute of the input elements to Property names on the parameter class:

class MyController : Controller
{
    public IActionResult Index([FromForm]Contact contact){ ... }
}

// ----
public class Contact
{
    public string Name {get;set;}
    public string PhoneNumber {get;set;}
}

But what about model binding an array — for instance, if you have a list of question inputs, and want to store the answers in a list?

If you name each field as if it were an array element:

 <fieldset id="q1">
     <label class="question">Question 1</label>
     <ul class="form-style">
        <li>
            <input type="radio" id="c1" name="answers[0]" value="@c1"/>
            <label for="c1">@c1</label>
        </li>
        <li>
            <input type="radio" id="c2" name="answers[0]" value="@c2"/>
            <label for="c2">@c2</label>
        </li>
    </ul>
  </fieldset>
    <fieldset id="q2">
        <label class="question">Question 1?</label>
        <ul class="form-style">
            <li>
                <input type="radio" id="b1" name="answers[1]" value="@b1"/>
                <label for="b1">@b1</label>
            </li>
            <li>
                <input type="radio" id="b2" name="answers[1]" value="@b2"/>
                <label for="b2">@b2</label>
            </li>
            <li>
                <input type="radio" id="b3" name="answers[1]" value="@b3"/>
                <label for="b3">@b3</label>
            </li>
            <li>
                <input type="radio" id="b4" name="answers[1]" value="@b4"/>
                <label for="b4">@b4</label>
            </li>
        </ul>
    </fieldset>

then AspNet will bind the submitted fields named answers[0], answers[1], ... to an array Property in your class with the matching name:

public class Questions
{
    public string[] Answers {get;set;}
}

.Net Core Strong Typed Configuration Binding for Arrays | Microsoft.Extensions.Configuration

The .Net Core Microsoft.Extensions.Configuration package includes the ConfigurationBinder.Get<>() method which can read a config file and bind name-value entries to a class. This gives you strongly typed configuration:

public class FromTo
{
    public string From { get; init; }
    public string To { get; init; }
}

The method is an extension method for Configuration so you can call it straight off a Configuration instance. In an Asp.Net Startup.cs class, for instance:

services.AddSingleton(
    s=>Configuration
            .GetSection("fromto").Get<FromTo>());

will result in your services collection knowing to provide an FromTo class with the Properties populated from the configuration entries, matched by section:propertyname :

{
  "fromto:from": "[email protected]",
  "fromto:to": "[email protected]"
}

or if you use secrets:

cd <projectdirectory>
dotnet user-secrets init
dotnet user-secrets set fromto:from [email protected]
dotnet user-secrets set fromto:to [email protected]

That works great for the usual primitives - string, numbers, boolean — but what about binding an Array?

public class FromTo
{
    public string From { get; init; }
    public string To[] { get; init; }
}

the From field is still bound but the To field silently fails and results in a null value.

The magic trick is to add a colon-separated number suffix to each setting name:

{
  "fromto:from": "[email protected]",
  "fromto:to:0": "[email protected]",
  "fromto:to:1": "[email protected]",
  "fromto:to:2": "[email protected]",
}

Now Configuration.GetSection("fromto").Get<FromTo>() will successfully bind all 3 rows for "fromto:to into the public string To[] array.

.Net and Asp.Net on MacOs & Mono instead of Windows (using Visual Studio or JetBrains Rider)

Mono goes a long way in running code written for .Net on Windows. It is all very much easier if you either start with cross-platform in mind, or if you move to .Net Core; but even for existing .Net Framework projects mono can run runs most things including Asp.Net.

Here's my checklist from a couple of years of opening .Net Framework solution files on a Mac and finding they don't build first time.

Most of these require you to edit the .csproj file to make it cross-platform, so a basic grasp of msbuild is very helpful.

  1. For AspNet: inside the PropertyGroup section near the top of the csproj file, add an element:

    <WebProjectOutputDir Condition="$(WebProjectOutputDir) == '' AND $(OS) == 'Unix' ">bin/</WebProjectOutputDir>

    Use this if you get a 'The “KillProcess” task was not given a value for the required parameter “ImagePath” (MSB4044)' error message; or if the build output shows you are trying to create files in an top-level absolute /bin/ path.

  2. For AspNet: Add Condition="'$OS'!='Unix'" to the reference to Microsoft.Web.Infrastructure.dll AND delete the file from the website bin directory.

    <Reference Condition="'$OS'!='Unix'" Include="Microsoft.Web.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
    <Private>True</Private>
    <HintPath>..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll</HintPath>
    </Reference>
  3. For all project types—but, only if you need to use the netCore dotnet build tooling to build an NetFramework project on unix. mono's msbuild does not need this. Add this section somewhere in the csproj file (I put it right at the bottom), to resolve NetFramework4 reference paths:

    <PropertyGroup Condition="$(TargetFramework.StartsWith('net4')) and '$(OS)' == 'Unix'">
    <!-- When compiling .NET SDK 2.0 projects targeting .NET 4.x on Mono using 'dotnet build' you -->
    <!-- have to teach MSBuild where the Mono copy of the reference asssemblies is -->
    <!-- Look in the standard install locations -->
    <BaseFrameworkPathOverrideForMono Condition="'$(BaseFrameworkPathOverrideForMono)' == '' AND EXISTS('/Library/Frameworks/Mono.framework/Versions/Current/lib/mono')">/Library/Frameworks/Mono.framework/Versions/Current/lib/mono</BaseFrameworkPathOverrideForMono>
    <BaseFrameworkPathOverrideForMono Condition="'$(BaseFrameworkPathOverrideForMono)' == '' AND EXISTS('/usr/lib/mono')">/usr/lib/mono</BaseFrameworkPathOverrideForMono>
    <BaseFrameworkPathOverrideForMono Condition="'$(BaseFrameworkPathOverrideForMono)' == '' AND EXISTS('/usr/local/lib/mono')">/usr/local/lib/mono</BaseFrameworkPathOverrideForMono>
    <!-- If we found Mono reference assemblies, then use them -->
    <FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net40'">$(BaseFrameworkPathOverrideForMono)/4.0-api</FrameworkPathOverride>
    <FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net45'">$(BaseFrameworkPathOverrideForMono)/4.5-api</FrameworkPathOverride>
    <FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net451'">$(BaseFrameworkPathOverrideForMono)/4.5.1-api</FrameworkPathOverride>
    <FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net452'">$(BaseFrameworkPathOverrideForMono)/4.5.2-api</FrameworkPathOverride>
    <FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net46'">$(BaseFrameworkPathOverrideForMono)/4.6-api</FrameworkPathOverride>
    <FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net461'">$(BaseFrameworkPathOverrideForMono)/4.6.1-api</FrameworkPathOverride>
    <FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net462'">$(BaseFrameworkPathOverrideForMono)/4.6.2-api</FrameworkPathOverride>
    <FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net47'">$(BaseFrameworkPathOverrideForMono)/4.7-api</FrameworkPathOverride>
    <FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net471'">$(BaseFrameworkPathOverrideForMono)/4.7.1-api</FrameworkPathOverride>
    <FrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != '' AND '$(TargetFramework)' == 'net472'">$(BaseFrameworkPathOverrideForMono)/4.7.2-api</FrameworkPathOverride>
    <EnableFrameworkPathOverride Condition="'$(BaseFrameworkPathOverrideForMono)' != ''">true</EnableFrameworkPathOverride>
    <!-- Add the Facades directory.  Not sure how else to do this. Necessary at least for .NET 4.5 -->
    <AssemblySearchPaths Condition="'$(BaseFrameworkPathOverrideForMono)' != ''">$(FrameworkPathOverride)/Facades;$(AssemblySearchPaths)</AssemblySearchPaths>
    </PropertyGroup>
  4. For projects that have lived through C# evolution from C# 5 to C# 7: You may need to remove duplicate references to e.g. System.ValueTuple. Add Condition="'$(OS)' != 'Unix'" to the reference. This applies to Types that MS put on NuGet.org during the evolution. idk why msbuild builds without complain on Windows but not on Unices.
    Example:

    <Reference Condition="'$(OS)' != 'Unix'" Include="System.ValueTuple, Version=4.0.1.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
    <HintPath>..\packages\System.ValueTuple.4.3.1\lib\netstandard1.0\System.ValueTuple.dll</HintPath>
    </Reference>
  5. For References to Microsoft.VisualStudio.TestTools.UnitTesting: Add a nuget reference to MSTEST V2 from nuget.org and make it conditional on the OS

    <ItemGroup Condition="'$(OS)' == 'Unix'">
    <Reference Include="MSTest.TestFramework" Version="2.1.1">
    <HintPath>..\packages\MSTest.TestFramework.2.1.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll</HintPath>
    </Reference>
    <Reference Include="coverlet.collector" Version="1.3.0" >
    <HintPath>..\packages\coverlet.collector.1.3.0\build\netstandard1.0\coverlet.collector.dll</HintPath>
    </Reference>
    </ItemGroup>

    Note this will only get you to a successful build. To run the tests on unix you then have to download and build https://github.com/microsoft/vstest and run it with e.g.
    mono ~/Source/Repos/vstest/artifacts/Debug/net451/ubuntu.18.04-x64/vstest.console.exe --TestAdapterPath:~/Source/Repos/vstest/test/Microsoft.TestPlatform.Common.UnitTests/bin/Debug/net451/ MyTestUnitTestProjectName.dll.

  6. Case Sensitivity & mis-cased references
    Windows programmers are used to a case-insensitive filesystem. So if code or config contains references to files, you may need to correct mismatched casing. Usually a 'FileNotFoundException' will tell you if you have this problem.

  7. The Registry, and other Permissions
    See this post for more: https://www.cafe-encounter.net/p1510/asp-net-mvc4-net-framework-version-4-5-c-razor-template-for-mono-on-mac-and-linux

Using the command line

It is helpful to be somewhat familiar with microsoft docs on MSBuild Concepts since msbuild will be issuing most of your build errors. If you have installed mono then you can run msbuild from the command line with extra diagnostics e.g.

msbuild -v:d >> build.log

you can also run web applications from the command just by running

xsp

from the project directory.

Original Text from 2011

For reasons best not examined too closely I switch between between Mac and PC which, since I earn my crust largely with .Net development, means switching between Visual Studio and MS.Net and MonoDevelop with Mono.

Mono is very impressive, it is not at all a half hearted effort, and it does some stuff that MS haven't done. But when switching environments, there's always the occasional gotcha. Here are some that have got me, and some solutions.

  • Gotcha: Linq Expressions don't work on Mono?
    Solution: Add a reference to System.Core to your project.
  • Question: What version of NUnit is built in to mono?As of Feb 2011, it's nunit 2.4.8.

Dev Azure Pipeline Web Config Transform

The web.config transform appears at first glance to be somewhat orphaned in Azure DevOps. If you are deploying to a WebApp then the pipeline wizard will probably have given you this snippet for your Build step:

- task: VSBuild@1
inputs:
solution: '$(solution)'
msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DesktopBuildPackageLocation="$(build.artifactStagingDirectory)\WebApp.zip" /p:DeployIisAppPath="Default Web Site"'
platform: '$(buildPlatform)'
configuration: '$(buildConfiguration)'

To run config transforms you can add the additional parameter /p:TransformConfigFile=true to your msbuildArgs.

The replacement, of course, is the independent FileTransform task (which can also work within zip files). But once your site is packaged the Web.Release.Config is left behind; so you have to run the FileTransform task before, not after, the build step. (Similarly the WebAppDeployment task can't transform a Web.Release.config that isn't inside the package).

Slightly magically, the default parameter for the FileTransform is exactly what you typically want for web.Release.config transforms:

#xmlTransformationRules: '-transform **\*.Release.config -xml **\*.config-transform **\*.$(Release.EnvironmentName).config -xml **\*.config' # Optional

FileTransform can also do variable substitution, and you can have multiple FileTransform steps in your pipeline. I also like to see some diagnostic feedback so I include script step to show the xml transform result. But I put it before variable substitution, because the variables often include secrets:

- task: FileTransform@1
displayName: Transform Release.Config
inputs:
folderPath: '$(System.DefaultWorkingDirectory)/Ui/'
enableXmlTransform: true
xmlTransformationRules: -transform **/*.Release.config -xml **/*.config

- task: PowerShell@2
displayName: "Confirm web.config transform"
inputs:
targetType: 'inline'
script: 'cat $env:System_DefaultWorkingDirectory/Ui/web.config'
errorActionPreference: 'continue'

- task: FileTransform@1
displayName: Variable substitution *.config
inputs:
folderPath: '$(System.DefaultWorkingDirectory)/Ui/'
fileType: 'xml'
targetFiles: '**/*.config'

- task: VSBuild@1
...etc..

Reference