Quantcast
Channel: Developer Express Inc.
Viewing all articles
Browse latest Browse all 2370

Microsoft Identity and XPO Continues: Support for .NET Core available

$
0
0

Some time ago I wrote an article on an XPO-based storage provider for Microsoft Identity. This provider is available as NuGet package, but at the time of writing, we didn’t have XPO available for .NET Core.

One of the things we released in v17.2 is XPO for .NET Core on which I did a webinar and a blog post. An obvious next step for the Identity Storage provider would be to make it  available on .NET Core as well.

Some additional goals to achieve

With the desire to support .NET Core as well as .NET Framework, there were some additional wishes I had with this project:

One code base for .NET Framework and .NET Core

Before I started with the .NET Core support, I had the impression that .NET Core version would be similar to the .NET Framework version. I was for 75% correct with that assumption.

After investigating the Identity source code on GitHub, it seemed that there is some extra functionality in the .NET Core version (Claims on Roles, and UserTokens). Also the signature on several interface methods was changed. (Cancellation token parameter)

As expected, the references in the .NET Framework version are different from the references for the .NET Core version.

One NuGet package for .NET Framework and .NET Core

Since the NuGet package format supports multi .NET version support, it would be nice to have everything packed up in one package.

Use .NET Core Dependency Injection to configure XPO and Storage provider

One of the sweet new things in ASP.NET Core is the Dependency Injection which comes with it. It seems obvious to use the D.I. to initialize the XPO datalayer as well as the storage provider in a similar way as the M.S. project template is doing for Entity Framework.

What needs to be done?

To have one project supporting both .NET Framework and .NET Core, the project file needs to be converted to the new csproj file format. We also need to change one of the default options to  support different .NET versions.

Change the contents of the csproj file to the new format

The old csproj file format looks like this:

<?xml version="1.0" encoding="utf-8"?><Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"><Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" 
       Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /><PropertyGroup><Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration><Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform><ProjectGuid>{4A9EC437-F8F0-4C53-B288-DE26E58F061F}</ProjectGuid><OutputType>Library</OutputType><AppDesignerFolder>Properties</AppDesignerFolder><RootNamespace>DX.Data.Xpo.Identity</RootNamespace><AssemblyName>DX.Data.Xpo.Identity</AssemblyName><TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion><FileAlignment>512</FileAlignment></PropertyGroup><PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "><DebugType>pdbonly</DebugType><Optimize>true</Optimize><OutputPath>bin\Release\</OutputPath><DefineConstants>TRACE</DefineConstants><ErrorReport>prompt</ErrorReport><WarningLevel>4</WarningLevel></PropertyGroup><ItemGroup><Reference Include="DevExpress.Data.v17.1, Version=17.1.5.0, Culture=neutral, 
         PublicKeyToken=b88d1754d700e49a, processorArchitecture=MSIL"><HintPath>..\packages\DevExpress.Data.17.1.5.0\lib\DevExpress.Data.v17.1.dll</HintPath></Reference><!-- the rest of the file is omitted -->

Together with a .nuspec file and a post build event, it is possible to build the project and the NuGet package in one go.

The new csproj format, looks much cleaner and it encapsulates the .nuspec file function right into the project. I think that’s a nice improvement. To start using this new file format, the most easy way is to unload the solution and edit the source code. The DX.Utils project will look like below:

<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><Version>1.0.0.6</Version><FileVersion>1.0.0.6</FileVersion><Authors>Don Wibier (DevExpress)</Authors><Description>Several C# utility classes and extension methods</Description><Copyright>Copyright (c) 2017 Don Wibier</Copyright></PropertyGroup><PropertyGroup><TargetFrameworks>netstandard2.0;net461</TargetFrameworks><PackageLicenseUrl>https://github.com/donwibier/DXWeb/blob/master/LICENSE</PackageLicenseUrl><PackageProjectUrl>https://github.com/donwibier/DXWeb/tree/master/DX.Utils</PackageProjectUrl><PackageIconUrl>https://www.devexpress.com/favicon.ico</PackageIconUrl><GeneratePackageOnBuild>true</GeneratePackageOnBuild><PackageTags>DXWeb Wibier DevExpress</PackageTags><PackageReleaseNotes>
      1.0.0.6: Changed .NET Framework to v4.6.1
      1.0.0.5: Initial dual mode package for .NET Framework and .NET Standard 2.0
Some features don't work yet on .NET Standard 2.0</PackageReleaseNotes><RunPostBuildEvent>OnBuildSuccess</RunPostBuildEvent></PropertyGroup><ItemGroup Condition="'$(TargetFramework)'=='net461'"><Reference Include="System" /><Reference Include="System.Configuration" /><Reference Include="System.Core" /><Reference Include="System.Drawing" /><Reference Include="System.Web" /><Reference Include="System.Xml.Linq" /><Reference Include="System.Data.DataSetExtensions" /><Reference Include="Microsoft.CSharp" /><Reference Include="System.Data" /><Reference Include="System.Xml" />    </ItemGroup><ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'"><PackageReference Include="System.Configuration.ConfigurationManager"><Version>4.4.1</Version></PackageReference></ItemGroup></Project>

I mentioned earlier that we need to make a small change in the default project format. It is the TargetFrameworks node; it contains both netstandard2.0 and net461. These values can be used for conditional ItemGroups to e.g. include framework dependent assembly and NuGet package references as well as source files. Because I included the Package information nodes, once I build the project, it will build the NuGet package automatically which will hold everything for .NET Framework and .NET Core.

Once we reload the project and open one of the cs files, you will notice a small addition to de code editor of VisualStudio. It is the combobox to select the framework you’re working on:

image

If we build the project now, you will have a NuGet package which includes the multi-version content:

image

Now we need to do some conditional coding because of the changed signature of the Interface methods which I have implemented on the XPUserStore as well as the XPRoleStore. On the XPUserStore there are some additional interfaces implemented for 2 Factor Authentication.

The easiest way to accomplish that is by using the #if (NETSTANDARD2_0) compiler directive to include or exclude specific code. Below is a fragement of the XPRoleStore which includes this conditional coding construct:

#if (NETSTANDARD2_0)
    public class XPRoleStore<TKey, TRole, TXPORole, TXPORoleClaim> : XpoStore<TXPORole, TKey>,
        IQueryableRoleStore<TRole>,
        IRoleClaimStore<TRole>
        where TKey : IEquatable<TKey>
        where TRole : XPIdentityRole<TKey, TXPORole, TXPORoleClaim>, IRole<TKey>
        where TXPORole : XPBaseObject, IDxRole<TKey>, IRole<TKey>
        where TXPORoleClaim: XPBaseObject, IDxRoleClaim<TKey>
#else
    public class XPRoleStore<TKey, TRole, TXPORole> : XpoStore<TXPORole, TKey>,
    	IQueryableRoleStore<TRole, TKey>
    	where TKey : IEquatable<TKey>
    	where TRole : XPIdentityRole<TKey, TXPORole>, IRole<TKey>
    	where TXPORole : XPBaseObject, IDxRole<TKey>, IRole<TKey>
#endif
{
    public XPRoleStore() : base()
    {}

    public XPRoleStore(string connectionName) :
      base(connectionName)
    {}
    public XPRoleStore(string connectionString, string connectionName) :
      base(connectionString, connectionName)
    {}
    public XPRoleStore(XpoDatabase database) :
      base(database)
    {}
#if (NETSTANDARD2_0)
    public async virtual Task<IdentityResult> CreateAsync(TRole role, 
						CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        await CreateAsync(role);
        return IdentityResult.Success;
    }
#endif
    public virtual Task CreateAsync(TRole role)
    {
      ThrowIfDisposed();
      if (role == null)
      {
        throw new ArgumentNullException("role");
      }
      return Task.FromResult(XPOExecute((db, wrk) =>
      {
        var xpoRole = XPOCreateRole(wrk);
        xpoRole.Assign(role, 0);
        return null;
      }));
    }
    // rest of the code is omitted ...
}

What you see is that the .NET Core method calls the actual original .NET Framework method to maintain the one codebase strategy.

Datamodel Changes

As I mentioned earlier, Identity for .NET Core has some additional functionality. The most noticeable ones are the support for Claims on Roles and the implementation of UserTokens. Another smaller detail is the use of normalized fields in certain circumstances.

What this means in that for essential lookup fields like username and email addresses, there is an extra field NormalizedEmail and NormalizedName. I implemented similar functionality in the first release of this provider by implementing the UserNameUpper and EmailUpper fields.

The idea behind this is quite straightforward. Whenever we need to lookup records by name or email, we use the Upper (or Normalized for .NET Core) for selecting. Remember that some database back ends are sorting and filtering case-sensitive.We don’t want this on the accounts and roles. A technical side effect is that the index files of the specific tables only contain uppercase characters, so less combinations to compare which improves the search speed.

The Normalized addition is even slightly better as the Upper approach I used. It should also remove the accents from characters which is common in certain languages to overcome sorting issues. .NET Core comes with a configurable Normalizer class which can be changed to suit your needs.

If we take a look at the adapted datamodel, it will look like below:

image

One of the really cool things with XPO is that the datamodel will be changed automatically in the database once we have added the extra classes and fields. No need for migrations.

Configuring Identity to use the XPO Storage provider

If we create an ASP.NET Core project through the project template, you can see in the ~/Startup.cs file how the storage provider for E.F. is configured.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();
    // Add application services.
    services.AddTransient<IEmailSender, EmailSender>();
    services.AddMvc();
}

The AddDbContext<T>(..) method is an extension method which comes with Entity Framework, and the .AddEntityFrameworkStores<ApplicationDbContext>() is another extension method which comes with the E.F. storage provider.

I have created similar extension methods to first configure the XPO Datalayer, and also an extension method which enables you to inject a UnitOfWork into your controller or other parts of you application which support DI.

public static class XpoCoreExtensions
{
    public static IServiceCollection AddXpoDatabase(this IServiceCollection serviceCollection, string connectionName, string connectionString)
    {
        return serviceCollection.AddSingleton<XpoDatabase>(new XpoDatabase(connectionString, connectionName));
    }
    public static IServiceCollection AddXpoUnitOfWork(this IServiceCollection serviceCollection, string connectionName)
    {
        return serviceCollection.AddScoped<UnitOfWork>((sp) => sp.GetService<XpoDatabase>().GetUnitOfWork());
    }
}

The extension method for configuring the storage provider is a bit more complicated. In the ConfigureServices method, there is a call to services.AddIdentity<ApplicationUser, IdentityRole>(). In our case, these 2 types specify the DTO classes for our Storage provider.

When configuring the XPO Storage provider, we need to pass in the persistent types for the ApplicationUser and Role. To follow the same approach, I’d like to do that through generic type parameters. The initialization will look like this:

services
        .AddIdentity<ApplicationUser, ApplicationRole>()
        .AddXpoIdentityStores<XpoApplicationUser, XpoApplicationRole>()                
        .AddDefaultTokenProviders();

The extension method contains some code to determine the generic types and pass those into the XPUserStore and XPRoleStore constructors. It furthermore contains some code to determine the types which where specified in the AddIdentity method. You can check how this is done by viewing the soure code on GitHub.

How to use this in your own .NET Core App

Thanks to the Dependency Injection in .NET Core, it’s pretty straightforward to configure the XPO Storage provider. Once you have created a new project or opened an existing one, you can remove all references to Entity Framework including the using statements. Also all code related to Entity Framework can be removed like the DBContext class and the migrations files. This is basically everything in the ~/Data folder.

Next make sure you have configured your environment to use your personal DevExpress NuGet feed as described here.

We can now add a reference to the DX.Data.Xpo.Identity NuGet package to the project. This will get all dependencies like DevExpress.Xpo as well.

In the ~/Models/ApplicationUser.cs file, replace the ApplicationUser class with the following code:

public class ApplicationUser : XPIdentityUser<XpoApplicationUser>
{
    public ApplicationUser(XpoApplicationUser source) : base(source)
    {}

    public ApplicationUser(XpoApplicationUser source, int loadingFlags) : base(source, loadingFlags)
    {}

    public ApplicationUser()
    {}

    public override void Assign(object source, int loadingFlags)
    {
        base.Assign(source, loadingFlags);
        //XpoApplicationUser src = source as XpoApplicationUser;
        //if (src != null)
        //{
        //	// additional properties here
        //	this.PropertyA = src.PropertyA;
        //	// etc.				
        //}
    }
}
  

And add the following code to the file as well:

public class ApplicationRole : XPIdentityRole<XpoApplicationRole>
{
    public ApplicationRole(XpoApplicationRole source, int loadingFlags) : base(source, loadingFlags)
    {}

    public ApplicationRole(XpoApplicationRole source) : base(source)
    {}

    public ApplicationRole()
    {}
    public override void Assign(object source, int loadingFlags)
    {
        base.Assign(source, loadingFlags);
        //XpoApplicationRole src = source as XpoApplicationRole;
        //if (src != null)
        //{
        //	// additional properties here
        //	this.PropertyA = src.PropertyA;
        //	// etc.				
        //}
    }
}

// This class will be persisted in the database by XPO
// It should have the same properties as the ApplicationUser
[MapInheritance(MapInheritanceType.ParentTable)]
public class XpoApplicationUser : XpoDxUser
{
    public XpoApplicationUser(Session session) : base(session)
    {
    }
    public override void Assign(object source, int loadingFlags)
    {
        base.Assign(source, loadingFlags);
        //ApplicationUser src = source as ApplicationUser;
        //if (src != null)
        //{
        //	// additional properties here
        //	this.PropertyA = src.PropertyA;
        //	// etc.				
        //}
    }
}

[MapInheritance(MapInheritanceType.ParentTable)]
public class XpoApplicationRole : XpoDxRole
{
    public XpoApplicationRole(Session session) : base(session)
    {
    }
    public override void Assign(object source, int loadingFlags)
    {
        base.Assign(source, loadingFlags);
        //ApplicationUser src = source as ApplicationUser;
        //if (src != null)
        //{
        //	// additional properties here
        //	this.PropertyA = src.PropertyA;
        //	// etc.				
        //}
    }
}
  

The last thing we need to do is to change the service configuration in ~/Startup.cs to include the following:

public void ConfigureServices(IServiceCollection services)
{
    //Initialize XPODataLayer / Database
    services
        .AddXpoDatabase("DefaultConnection", Configuration.GetConnectionString("DefaultConnection"));
    //Initialize identity to use XPO
    services
        .AddIdentity<ApplicationUser, ApplicationRole>(options => {
            options.Lockout.AllowedForNewUsers = true;
            options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
            options.Lockout.MaxFailedAccessAttempts = 3;
        })
        .AddXpoIdentityStores<XpoApplicationUser, XpoApplicationRole>()                
        .AddDefaultTokenProviders();

    // Add other application services.
    services.AddTransient<IEmailSender, EmailSender>();
    services.AddMvc();
}

With these changes in place, we're ready to boot up our app running on XPO!

Did I mention that the source of this project is available on GitHub? Feel free to clone this repo and make a pull request in case you encounter an issue.


Viewing all articles
Browse latest Browse all 2370

Trending Articles