NuGet ContentFiles

Creating a properly authored NuGet package from a SDK-style .csproj file is easy as long as only the primary output assembly should be packed. But things get tricky when it comes to arbitrary content like text or configuration files that should flow seamlessly into the output folder of consuming projects. The following tricks will help.

Build Action “Content”

First of all, change Build Action from “None” to “Content”.

This corresponds to a Content item within the .csproj file.

Copy to Output Directory “Copy if newer”

You might also want to set Copy to Output Directory to “Copy if newer” so that the file is deployed to the output directory when the project is built.

This corresponds to the CopyToOutputDirectory=”PreserveNewest” metadata within the .csproj file.

Note: Never ever use “Copy always” (which corresponds to CopyToOutputDirectory=”Always”)! This will result in an “unstable” build.

PackageCopyToOutput=”true”

Next you need to set PackageCopyToOutput=”true” directly within the .csproj file (there is no Visual Studio property for this).

Only then will the contentFiles section of the auto-generated .nuspec file have the copyToOutput=”true” attribute set.

So far so good. If you now generate a NuGet package out of your .csproj file using e.g. the dotnet pack command or by setting <GeneratePackageOnBuild>true</GeneratePackageOnBuild>, the content file will be copied to the output directory of a consuming project.

But this does only work for direct PackageReferences. It will fail for transitive dependencies.

ProjectReference PrivateAssets=”analyzers;build”

Here is the trick: To make content files flow seamlessly across transitive dependencies, the intermediate referencing project must set PrivateAssets=”analyzers;build” (or anything that does not include contentFiles) on the ProjectReference.

This is because by contentFiles are treated as a private asset by default. Private means that the data goes only into the immediately referencing project, but does not flow across transitive dependencies.

Set PrivateAssets as shown above will result in the following include attribute inside the dependency element of the resulting .nuspec file:

Note how this includes the ContentFiles value which is otherwise excluded by default.

References

Azure Pipelines deploy from multiple projects to same environment

There is a problem in Azure Pipelines when trying to deploy to the same target environment from multiple Azure DevOps projects (within the same organization). Even though Azure DevOps allows provisioning a single “deployment pool” to multiple projects, you still need to install one Azure Agent for each project on the same target machine.

The problem here is that the generated script (which you need to copy to the clipboard and run in an administrator PowerShell on the target machine) generates a conflict on the generated Windows service name. This will not only fail during installation of the 2nd agent on the target machine, it will also break an already running agent on the same machine.

As a workaround you need to patch the --agent $env:COMPUTERNAME part of the generated script, e.g. by appending the name of the 2nd project. Example:

--agent $env:COMPUTERNAME-MyOtherProject

See Azure DevOps: How to manage VM agents when multiple projects deploy to the same virtual machine (Stack Overflow) for more details.

Visual Studio 2019 MSBuild Error when Building .sln

Problem Description

After completely cleaning up all Visual Studio installations and reinstalling Visual Studio 2019 Version 16.8.7, I was no longer able to build solutions (*.sln) using MSBuild from the command line (“Developer Command Prompt for VS 2019”). I got an error when executing the following commands:

md test
cd test
dotnet new sln
msbuild test.sln
Microsoft (R) Build Engine version 16.8.3+39993bd9d for .NET Framework
Copyright (C) Microsoft Corporation. All rights reserved.

Building the projects in this solution one at a time. To enable parallel build, please add the "-m" switch.
Build started 13.03.2021 09:20:25.
C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\SolutionFile\ImportAfter\Microsoft.NET.Sdk.Solution.targets(14,3): error MSB4019: The imported project "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.Common.targets" was not found. Confirm that the expression in the Import declaration "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.Common.targets" is correct, and that the file exists on disk.

Build FAILED.

  C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\SolutionFile\ImportAfter\Microsoft.NET.Sdk.Solution.targets(14,3): error MSB4019: The imported project "C:\Program Files (x86)\Microsoft Visual Studio\2019
\Professional\MSBuild\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.Common.targets" was not found. Confirm that the expression in the Import declaration "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.Common.targets" is correct, and that the file exists on disk.

    0 Warning(s)
    1 Error(s)

Time Elapsed 00:00:00.08

Solution

The error could be fixed by deleting the following file:

C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\SolutionFile\ImportAfter\Microsoft.NET.Sdk.Solution.targets

Background Information

Not sure where this file was coming from and if it is needed for other purposes. Just for reference, here is the content of this file:

<!--
***********************************************************************************************
Microsoft.NET.Sdk.Solution.targets

WARNING:  DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
          created a backup copy.  Incorrect changes to this file will make it
          impossible to load or build your projects from the command-line or the IDE.

Copyright (c) .NET Foundation. All rights reserved.
***********************************************************************************************
-->
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <Import Project="$(MSBuildSDKsPath)\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.Common.targets" />
  
  <!--
  ============================================================
                              _CheckForSolutionLevelRuntimeIdentifier

    Ensure that a runtime identifier is not specified at the solution level
  ============================================================
   -->
  <Target Name="_CheckForSolutionLevelRuntimeIdentifier"
          BeforeTargets="Build"
          Condition="'$(RuntimeIdentifier)' != ''">

    <NETSdkError ResourceName="CannotHaveSolutionLevelRuntimeIdentifier" />

  </Target>

</Project>

It’s important to note that no MSBuildSDKsPath environment variable was set, which is as it should be. For some reason, MSBuild determined it to be C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Sdks. This folder exists, but it doesn’t contain the expected Microsoft.NET.Sdk subfolder. This subfolder only resides in the various C:\Program Files\dotnet\sdk\{version}\Sdks directories.

Obviously MSBuild includes all files from C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\SolutionFile\ImportAfter (no matter how they are named) and tries to process them when a solution (*.sln) is built. For some reason, the malicious Microsoft.NET.Sdk.Solution.targets file creeped into the file system and broke the build.

Howto turn Windows 8 UAC really off

I found some valuable information about turning UAC in Windows 8 really off. By “really” I mean that UAC is still partially active in Windows 8 even when move the slider to “Never notify” in the “User Account Control Settings” panel.

Essentially you have to change the following registry value and reboot:

HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System\EnableLUA=0 (DWORD)

Unfortunately this breaks accessibility to the Windows Store so a better solution is this:

HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System\EnableLUA=1 (back to default)
HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System\ConsentPromptBehaviorAdmin=0 (default is 2)
HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System\EnableUIADesktopToggle=1 (default is 0)

More information about Windows 8 UAC here:

http://www.neowin.net/forum/topic/1120770-windows-8-run-everything-as-administrator/

TortoiseSVN Proxy Exceptions for Local Host

Today I found some strange behavior in TortoiseSVN (1.7.11, Build 23600 – 64 Bit).

I tried to configure the proxy settings so that I can both access SVN repos hosted on the internet behind an HTTP proxy and also local repositories hosted on the dev machine. It was unclear what to enter into the “Exceptions” field.

“localhost” didn’t work. “127.0.0.1” as well as the local IPv4 or local IPv6 addresses didn’t work either. The hostname as printed by the “hostname” command (here: “XPS15z”) in a command shell did also not work.

The only thing that worked was the hostname in lowercase letters (here: “xps15z”)!

TortoiseSVN Proxy Exceptions

Maybe this worked because the SVN URL for the local repo also used lowercase letter. Strange bug. Host names should be case insensitive.

UAC, Elevation and the default Administrator account

These days I was struggling with Windows 7 UAC (User Account Control). We have an (.NET) app where we want to detect if we are running as an elevated process (if UAC is turned on), or if we are a member of the Administrators group (fallback solution when UAC is turned off). There is a lot of good information including C# sample code around (see e. g. here). They recommend to check the registry for the EnableLUA flag to determine if UAC is turned on, and then call OpenProcessToken and GetTokenInformation(…, TokenElevationType, …) to retrieve the elevation type of the current process.

The problem with all these solutions is that they assume that – when UAC is turned on – the elevation will be either TokenElevationTypeLimited (when not running elevated) or TokenElevationTypeFull (when running elevated). But this is not true! There are times when you receive TokenElevationTypeDefault even though you might expect the value to be TokenElevationTypeFull.

You will receive TokenElevationTypeDefault even when UAC is turned on when the following preconditions are met:

  1. The default Administrator account is enabled (default: disabled), and
  2. The security policy “User Account Control: Use Admin Approval Mode for the built-in Administrator account” is disabled. This is the default.

You can use the Local Security Policy Editor (secpol.msc) and navigate to Security Settings – Local Policies – Security Options to verify this option.

When the above prerequisites are met, you will receive TokenElevationTypeDefault when one of the following conditions is true:

  1. You are running as the default Administrator, or
  2. You started the process through the “Run as administrator” Explorer context menu and selected the default Administrator account.

In other words: When the security option “User Account Control: Use Admin Approval Mode for the built-in Administrator account” is disabled, the default Administrator account behaves as if UAC was also disabled.

To make our app behave the same no matter which Administrator account is used (the default one, or any of the other existing accounts which are a member of the Administrators group), I enhanced the solution for determining process elevation as follows:

/// <summary>
/// Base on code found here:
/// http://stackoverflow.com/questions/1220213/c-detect-if-running-with-elevated-privileges
/// </summary>
public static class UacHelper
{
  private const string uacRegistryKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System";
  private const string uacRegistryValue = "EnableLUA";

  private const uint STANDARD_RIGHTS_READ = 0x00020000;
  private const uint TOKEN_QUERY = 0x0008;
  private const uint TOKEN_READ = (STANDARD_RIGHTS_READ | TOKEN_QUERY);

  [DllImport("advapi32.dll", SetLastError = true)]
  [return: MarshalAs(UnmanagedType.Bool)]
  static extern bool OpenProcessToken(IntPtr ProcessHandle, UInt32 DesiredAccess, out IntPtr TokenHandle);

  [DllImport("advapi32.dll", SetLastError = true)]
  public static extern bool GetTokenInformation(IntPtr TokenHandle, TOKEN_INFORMATION_CLASS TokenInformationClass, 
                                                IntPtr TokenInformation, uint TokenInformationLength, 
                                                out uint ReturnLength);

  public enum TOKEN_INFORMATION_CLASS
  {
    TokenUser = 1,
    TokenGroups,
    TokenPrivileges,
    TokenOwner,
    TokenPrimaryGroup,
    TokenDefaultDacl,
    TokenSource,
    TokenType,
    TokenImpersonationLevel,
    TokenStatistics,
    TokenRestrictedSids,
    TokenSessionId,
    TokenGroupsAndPrivileges,
    TokenSessionReference,
    TokenSandBoxInert,
    TokenAuditPolicy,
    TokenOrigin,
    TokenElevationType,
    TokenLinkedToken,
    TokenElevation,
    TokenHasRestrictions,
    TokenAccessInformation,
    TokenVirtualizationAllowed,
    TokenVirtualizationEnabled,
    TokenIntegrityLevel,
    TokenUIAccess,
    TokenMandatoryPolicy,
    TokenLogonSid,
    MaxTokenInfoClass
  }

  public enum TOKEN_ELEVATION_TYPE
  {
    TokenElevationTypeDefault = 1,
    TokenElevationTypeFull,
    TokenElevationTypeLimited
  }

  private static bool? _isUacEnabled;
  public static bool IsUacEnabled
  {
    get
    {
    if (_isUacEnabled == null)
    {
      var uacKey = Registry.LocalMachine.OpenSubKey(uacRegistryKey, false);
      if (uacKey == null)
      {
      _isUacEnabled = false;
      }
      else
      {
      var enableLua = uacKey.GetValue(uacRegistryValue);
      _isUacEnabled = enableLua.Equals(1);
      }
    }
    return _isUacEnabled.Value;
    }
  }

  private static bool? _isAdministrator;
  public static bool IsAdministrator
  {
    get
    {
    if (_isAdministrator == null)
    {
      var identity = WindowsIdentity.GetCurrent();
      Debug.Assert(identity != null);
      var principal = new WindowsPrincipal(identity);
      _isAdministrator = principal.IsInRole(WindowsBuiltInRole.Administrator);
    }
    return _isAdministrator.Value;
    }
  }

  private static bool? _isProcessElevated; 
  public static bool IsProcessElevated
  {
    get
    {
    if (_isProcessElevated == null)
    {
      if (IsUacEnabled)
      {
      var process = Process.GetCurrentProcess();

      IntPtr tokenHandle;
      if (!OpenProcessToken(process.Handle, TOKEN_READ, out tokenHandle))
      {
        throw new ApplicationException("Could not get process token.  Win32 Error Code: " + Marshal.GetLastWin32Error());
      }

      var elevationResult = TOKEN_ELEVATION_TYPE.TokenElevationTypeDefault;

      var elevationResultSize = Marshal.SizeOf((int)elevationResult);
      uint returnedSize;
      var elevationTypePtr = Marshal.AllocHGlobal(elevationResultSize);

      var success = GetTokenInformation(tokenHandle, TOKEN_INFORMATION_CLASS.TokenElevationType, elevationTypePtr, (uint)elevationResultSize, out returnedSize);
      if (!success)
      {
        Marshal.FreeHGlobal(elevationTypePtr);
        throw new ApplicationException("Unable to determine the current elevation.");
      }

      elevationResult = (TOKEN_ELEVATION_TYPE)Marshal.ReadInt32(elevationTypePtr);
      Marshal.FreeHGlobal(elevationTypePtr);

      // Special test for TokenElevationTypeDefault.
      // If the current user is the default Administrator, then the
      // process is also assumed to run elevated. This is assumed 
      // because by default the default Administrator (which is disabled by default) 
      //  gets all access rights even without showing a UAC prompt.
      switch (elevationResult)
      {
        case TOKEN_ELEVATION_TYPE.TokenElevationTypeFull:
        _isProcessElevated = true;
        break;
        case TOKEN_ELEVATION_TYPE.TokenElevationTypeLimited:
        _isProcessElevated = false;
        break;
        default:
        // Will come here if either
        // 1. We are running as the default Administrator.
        // 2. We were started using "Run as administrator" from a non-admin
        //    account and logged on as the default Administrator account from
        //    the list of available Administrator accounts.
        //
        // Note: By default the default Administrator account always behaves 
        //       as if UAC was turned off. 
        //
        // This can be controlled through the Local Security Policy editor 
        // (secpol.msc) using the 
        // "User Account Control: Use Admin Approval Mode for the built-in Administrator account"
        // option of the Security Settings\Local Policies\Security Options branch.
        _isProcessElevated = IsAdministrator;
        break;
      }
      }
      else
      {
      _isProcessElevated = IsAdministrator;
      }
      }
      return _isProcessElevated.Value;
    }
  }
}

HOWTO: Uninstall broken Windows 8 apps

I had a problem with a Windows 8 app (the “Flow” game) which was partially broken. The app’s tile was no longer shown on the start screen, and no icon was shown when I searched for the app’s name, though Windows noted that there was “1 Application” with a matching name – but no tiles where shown. For this reason, I couldn’t right-click the tile to uninstall it.

Thanks to MS support I learned that apps can be uninstalled manually through some PowerShell commands. They pointed me to this thread.

List all installed apps:

Get-AppxPackage –AllUsers

Remove a specific app:

Remove-AppxPackage FullPackageName

Hint: Windows 8 apps are placed in a hidden folder named C:\Program Files\WindowsApps. Simply deleting (or renaming) an app’s folder will not properly uninstall it.

WPF button in WPF Bing Maps MapItemsControl not reacting to touch events

Today I a solved problem with buttons placed on a MapItemsControl inside a WPF Bing Maps control not reacting to touch events. The full description of the problem and also the solution (custom TouchButton class with OnTouchDown, OnTouchMove and OnTouchUp overrides) can be found in the Bing Maps support forum.

Here’s the code:

 public class TouchButton : Button
  {
    protected override void OnTouchDown(TouchEventArgs e)
    {
      if (e.TouchDevice.Capture(this))
      {
        IsPressed = true;
        e.Handled = true;
      }
      base.OnTouchDown(e);
    }

    protected override void OnTouchMove(TouchEventArgs e)
    {
      if (e.TouchDevice.Captured == this)
      {
        // Update IsPressed property to indicate if the
        // current touch position is within the element's
        // visuals.
        IsPressed = IsHitTest(e.GetTouchPoint(this).Position);
        e.Handled = true;
      }
      base.OnTouchMove(e);
    }

    protected override void OnTouchUp(TouchEventArgs e)
    {
      if (e.TouchDevice.Captured == this)
      {
        ReleaseTouchCapture(e.TouchDevice);
        IsPressed = false;

        // Raise Click event if touch up occurs within
        // the element's visuals.
        if (IsHitTest(e.GetTouchPoint(this).Position))
        {
          OnClick();
        }

        e.Handled = true;
      }
      base.OnTouchUp(e);
    }

    private bool IsHitTest(Point point)
    {
      var hitTestResult = VisualTreeHelper.HitTest(this, point);
      return hitTestResult != null && hitTestResult.VisualHit != null;
    }

Windows 8 Beta (“Consumer Preview”) Experiences

I’d like to summarize my so far very positive experiences with the Windows 8 Beta (“Consumer Preview”) here.

I downloaded and installed the 64-bit en-US version on my XPS15z laptop into a virtual disk (.vhd file). The download was very fast (on an average download rate of 6.2 MB/s!), so it took less than 10 minutes to get it from the Microsoft servers.

Installation was seemless despite the extra hops that need to be taken to get it installed into a .vhd file. I followed the Guide to Installing and Booting Windows 8 Developer Preview off a VHD (Virtual Hard Disk) by Scott Hanselman for the Windows 8 Developer Preview and it still works exactly as described with the Beta. The only thing you need to be aware off is that the drive letter where you created the .vhd file upfront might change from C: to D: (in my case).

Windows 8 benefits over Windows 7

1. Hardware Support

Windows 8 seamlessly recognized the most important of the XPS15z’s quite modern hardware components, namely the Intel WLAN adapter, the Atheros Ethernet adapter, and the USB 3.0 host controller. On Windows 7 it was a PITA to get the drivers installed if you have neither access to USB, nor to a network and you don’t have a driver DVD handy.

Also, my network enabled EPSON BX635FWD printer was automatically detected, and the EPSON driver software was automatically installed.

Very nice experience!

2. Out-of-the-box support for mounting .ISO images

No more need to download and install Virtual Clone Drive or alike.

3. Mouse Without Borders

Installation Mouse Without Borders (one of my favorite tools) was a bit bumpy because this software is dependent on the .NET framework 2.0. Windows 8 comes with .NET framework 4.5 preinstalled, but this does not include the older .NET framework 3.5.1 (which includes 2.0). Installation through the “Programs and Features” applet failed, maybe because Internet was only available through the company proxy.

But I could get .NET framework 3.5.1 by mounting the downloaded Windows 8 installation .ISO image (which was a snap thanks to the integrated .ISO support) and executing the following command:

Dism.exe /online /enable-feature /featurename:NetFX3 /All /Source:F:\sources\sxs /LimitAccess