I don't think that this is a gitlab-ci issue. Focus your debugging efforts on executing that MSBuild command on the server (or within the container) associated with the runner you're using.
I recommend using before_script to call out to a script that verifies the dependencies needed by your pipeline. For example, your pre-build script could verify that MSBuild, NuGet, and Git are on the path of your runner/VM/Container (this lets you control these prerequisites using Puppet or Powershell DSC rather than managing them within the pipeline).
nuget restore should be called as part of a restore stage rather than in before_script. The restored packages can be passed between stages as artifacts.
You are double-quoting your invocation of MSBuild in your build job. Here's how I'm invoking MSBuild in a pipeline I'm currently working with:
build_dev:
stage: build
script:
- MSBuild $env:TARGETS /t:Build /p:Configuration=Debug
artifacts:
paths:
- ./*/bin
<<: *default_expiration
<<: *default_tags
In this scenario, my before_script has verified that MSBuild is on the path, so I'm free to use it. It might refer to MSBuild 12.0 or 14.0 depending on the runner. $env:TARGETS is from a gitlab-ci variable; it points to an MSBuild file, build.targets.
I believe you're using a basic shell executor; some things get easier when you specify shell = powershell in your runner config, Powershell syntax is flat out better for piecing together pipelines.
Finally, whether or not MSBuild automatically restores NuGet dependencies is really up to the way your .csproj files are written. I would argue that it's best to have nuget restore as a separate step. Having a separate step comes in handy when, for example, you need to restore Nuget packages that are used by MSBuild itself.
Verify that simply running MSBuild against your solution file automatically restores NuGet packages by running MSBuild yourself in the command line. I don't think gitlab-ci will affect the success of that command.