Migrating a Cross-targeted project.json .NET Core Library to csproj Using the .NET Core CLI

Today I migrated several of our internal C# libraries we use at CSG that are using .NET Core from the project.json to csproj format using the .NET Core CLI tools. Everything went well, but I ran into one snag with trying to build the NuGet packages. Here are my steps I went through to migrate and resolve the errors I encountered.

First, I started with deleting the global.json file that was being used to pin the sdk/cli tooling to a pre-csproj version. I opened the project in the command line and ran the migration command, which is dotnet migrate.

dotnet-migrate

So far so good. I went ahead and tried to build a Release build with a NuGet package by running dotnet build –configuration Release, which is what our continuous integration build script is doing to generate the NuGet packages.

dotnet-build

Well crap, the build command was failing with the following errors:

error MSB4018: The "PackTask" task failed unexpectedly.
error MSB4018: System.IO.FileNotFoundException: File not found: '...\bin\Release\net461\Csg.Data.Dapper.dll'.

There were some various other flavors of this error involving the netstandard1.6 path instead of net461, but they were all similar. It appeared to be failing when trying to build the nupkg because of missing build output files.  What was weirder, is that running the Build command (CTRL-SHIFT-B) in Visual Studio 2017 26228.4 was showing the same errors in the Output window, but the build was completing with a “Build: 1 succeeded, 0 failed, 0 skipped”. It wasn’t picking up on the fact that the dotnet pack command was failing. This seems like an unrelated problem with VS2017, so I went back to the command line build. I tried a view variations of dotnet build using the –framework argument, which led me to believe it was some issue with the multi-targeting being used in the project.

These projects are targeting both the full .NET Framework 4.6.1 and as well as NET Standard 1.6. Before the migration, the frameworks section of the project.json looked like this:

"frameworks": {
  "netstandard1.6": {
    "dependencies": {
      "NETStandard.Library": "1.6.0"
    }
  },
  "net461": {
  }
}

It’s worth mentioning here that there is no support via the Visual Studio 2017 UI (Project properties page) for configuring multiple target frameworks as mentioned by Damian Edwards in one of the recent ASP.NET Community Standup’s. The equivalent csproj syntax looks like this:

<PropertyGroup>
...
  <Authors>Justin R. Buchanan</Authors>
  <TargetFrameworks>netstandard1.6;net461</TargetFrameworks>
  <DebugType>portable</DebugType>
...
</PropertyGroup>

Note the framework list is semi-colon delimited in a <TargetFrameworks> element instead of <TargetFramework>.

While I was in the csproj I noticed it had migrated this project.json scripts content:

"scripts": {
  "postcompile": [
    "dotnet pack --no-build --configuration %compile:Configuration%"
  ]
}

to this csproj equivalent:

<Target Name="PostcompileScript" AfterTargets="Build" Condition=" '$(IsCrossTargetingBuild)' != 'true' ">
  <Exec Command="dotnet pack --no-build --configuration $(Configuration)" />
</Target>

I removed the new <Target Name=”PostcompileScript”> tag entirely, and then manually ran dotnet build and then dotnet pack, and yay, it worked!

EDIT: I was reading over this again today and noticed the Condition on the <Target> that seems like it should have prevented me from having this error (althought it wouldn’t have generated a NuGet package). I’ll have to do more research on exactly when $(IsCrossTargetingBuild) evaluates to true. It seems like I would have still needed to add the new dotnet pack step outlined below though.

dotnet-build-yay 
dotnet-pack-yay

The only change I had to make at this point was to to add an extra step to our build.cmd (used by our build server) to run the dotnet pack command. Previously we had not needed this because the dotnet build command was running the pack in the postcompile steps. Now it looks something like this:

SET SOLUTION=Csg.Data.Dapper.sln
SET BUILD_CONFIG=Release
...
dotnet build %SOLUTION% --configuration %BUILD_CONFIG%
...
dotnet pack %SOLUTION% --no-build --configuration %BUILD_CONFIG%
...

I’m not sure if this is a bug, or if I’m doing something else wrong, or both, but this got me past the error and I was able to continue migrating the rest of our libraries from project.json to csproj.

Authenticate ASP.NET Core Identity Users via Active Directory or LDAP Password

Update: I have published an updated 2.0.0-preview00 release that supports ASP.NET Core Identity 2.0 on .NET Standard 2.0 at NuGet.org. I'll publish 2.0.0 without the "preview" tag once I hear back from a couple folks that this resolved their reported issues.

In a project I was recently working on, I needed a way to store and manage user accounts in a stock ASP.NET Core Identity Entity Framework Core based database, but validate user passwords against an existing Active Directory domain. In this situation, I could not leverage Kerberos/Windows Authentication because users were outside the Intranet, nor could I use ADFS or equivalent SSO services as it was beyond the scope of my project to deploy such a solution.

To achieve this, I created a simple UserManager wrapper class that overrides the base CheckPasswordAsync method with one that uses the Novell LDAP library for NETStandard 1.3 to perform an LDAP bind against a directory, and thus perform simple password validation.

I began by creating a UserManager class that inherits from Microsoft.AspNetCore.Identity.UserManager.

/// <summary>
/// Provides a custom user store that overrides password related methods to valid the user's password against LDAP.
/// </summary>
/// <typeparam name="TUser"></typeparam>
public class LdapUserManager<TUser> : Microsoft.AspNetCore.Identity.UserManager<TUser>
where TUser: class

Then I implement CheckPasswordAsync() using an LdapAuthentication class, which is just a loose abstraction around the Novell LDAP library.

/// <summary>
/// Checks the given password agains the configured LDAP server.
/// </summary>
/// <param name="user"></param>
/// <param name="password"></param>
/// <returns></returns>
public override async Task<bool> CheckPasswordAsync(TUser user, string password)
{
    using (var auth = new LdapAuthentication(_ldapOptions))
    {
        string dn;

        // This gives a custom way to extract the DN from the user if it is different from the username.
        if (this.Store is IUserLdapStore<TUser>)
        {
            dn = await((IUserLdapStore<TUser>)this.Store).GetDistinguishedNameAsync(user);
        }
        else
        {
            dn = await this.Store.GetNormalizedUserNameAsync(user, CancellationToken.None);
        }

        if (auth.ValidatePassword(dn, password))
        {
            return true;
        }
    }

    return false;
}

The meat of the LdapAuthentication class is in the ValidatePassword() method.

/// <summary>
/// Gets a value that indicates if the password for the user identified by the given DN is valid.
/// </summary>
/// <param name="distinguishedName"></param>
/// <param name="password"></param>
/// <returns></returns>
public bool ValidatePassword(string distinguishedName, string password)
{
    if (_isDisposed)
    {
        throw new ObjectDisposedException(nameof(LdapConnection));
    }

    if (string.IsNullOrEmpty(_options.Hostname))
    {
        throw new InvalidOperationException("The LDAP Hostname cannot be empty or null.");
    }

    _connection.Connect(_options.Hostname, _options.Port);

    try
    {
        _connection.Bind(distinguishedName, password);
        return true;
    }
    catch (Exception ex)
    {
        System.Diagnostics.Debug.WriteLine(ex.Message);
        return false;
    }
    finally
    {
        _connection.Disconnect();
    }
}

At this point, I just needed some basic configuration and DI code to get things wired up in the Startup.cs of an ASP.NET Core app.

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<Justin.AspNetCore.LdapAuthentication.LdapAuthenticationOptions>(this.Configuration.GetSection("ldap"));
    services.AddLdapAuthentication<ApplicationUser>();
    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddUserManager<Justin.AspNetCore.LdapAuthentication.LdapUserManager<ApplicationUser>>()
        .AddEntityFrameworkStores<ApplicationDbContext>()                
        .AddDefaultTokenProviders();
}

This expects configuration to come from an AppSettings.json section, which looks like this:

"ldap": {
  "Hostname": "dc1.example.com",
  "Port": 389
}

This allows me to keep the user accounts in a database (in this instance, a MySQL database), but eliminates the need for the user to have a separate password. It’s important to note that in my case, users do not need to be able to change, reset, or otherwise manage their user account password through the web interface, as they have a separate existing process in place for that.

I intend on coming back at some point an implementing more of the UserManager methods that *can* be implemented via LDAP, but for now all I needed was to eliminate the need for users to create a separate account password for this app.

The full source code is available on GitHub, or  you can install the NuGet package:

Install-Package -Pre Justin.AspNetCore.LdapAuthentication