Myriota device configuration

For a couple of weeks Myriota Developer Toolkit has been sitting under my desk and today I got some time to setup a device, register it, then upload some data.

Myriota Developer Toolkit

The first step was to download and install the Myriota Configurator so I could get the device registration information and install the tracker example application.

Using Windows File Explorer to “unblock” the downloaded file

After “unblocking” the zip file and upgrading my pip install the install script worked.

Myriota Configurator installation script

The application had to be run from the command line with “python MyriotaConfigurator.py”

Myriota Configurator main menu
Myriota Configurator retrieving device registration code

On the device I’m using the Tracker sample application to generate some sample payloads.

Myriota Configurator downloading tracker sample to device

The next step was to “register” my device and configure the destination(s) for its messages.

Myriota Device Manager Device configuration

Once the device and device manager configuration were sorted, I put the Tracker out on the back lawn on top of a large flowerpot.

Device Manager Access Times

On the “Access Times” page I could see that there were several periods when a satellite was overhead and overnight a couple of messages were uploaded.

ASP.NET Core authentication – In the beginning

While building my ASP.NET Core identity, Dapper Custom storage provider I found there wasn’t a lot of discussion of the ASPNETUserClaims functionality for fine “grained permissions”.

ASP.NET Core identity initial data model

ASP.NET Core identity Roles can also have individual claims but with the authorisation model of the legacy application I work on this functionality hasn’t been useful. We use role based authentication with a few user claims to minimise the size of our Java Web Tokens(JWT)

Visual Studio 2022 ASP.NET Core Web Application template options

The first step was to create a “bare-bones” ASP.NET Core Razor pages Web Application with Individual Accounts Authentication project

Default ASP.NET Core identity Web application Homepage

I tried to minimise the modifications to the application. I added EnableRetryOnFailure, some changes to names spaces etc. I also added support for email address confirmation with SendGrid and “authentication” link to the navabar in _Layout.cshtml.

@page
@model RolesModel
@{
    <table class="table">
        <thead>
            <tr>
                <th>Role</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var role in Model.Roles)
            {
                <tr>
                    <td>
                        @Html.DisplayFor(modelItem => role.Value)
                    </td>
                </tr>
            }
        </tbody>
    </table>
    <br/>
        <table class="table">
        <thead>
            <tr>
                <th>Claim Subject</th>
                <th>Value</th>
            </tr>
        </thead>

        <tbody>
            @foreach (var claim in Model.Claims)
            {
                <tr>
                    <td>
                        @Html.DisplayFor(modelItem => claim.Type)
                    </td>
                    <td>
                        @Html.DisplayFor(modelItem => claim.Value)
                    </td>
                </tr>
            }
        </tbody>
    </table>
}

The “Authentication” page displays the logged in User’s Role and Claims.

namespace devMobile.AspNetCore.Identity.WebApp.EFCore.Pages
{
    [Authorize()]
    public class RolesModel : PageModel
    {
        private readonly ILogger<RolesModel> _logger;

        public List<Claim> Roles { get; set; }
        public List<Claim> Claims { get; set; }

        public RolesModel(ILogger<RolesModel> logger)
        {
            _logger = logger;
        }

        public void OnGet()
        {
            Roles = User.Claims.Where(c => c.Type == ClaimTypes.Role).ToList();

            Claims = User.Claims.Where(c => c.Type != ClaimTypes.Role).ToList();
        }
    }
}

Each user can have role(s), with optional claims, and some optional individual claims.

ASP.NET Core identity application Authentication information page

The WebApp.EFCore project is intended to be the starting point for a series of posts about ASP.NET Core identity so I have not included Cross-Origin Resource Sharing (CORS), Cross Site Request Forgery (CSRF) etc. functionality.

.NET Core web API + Dapper – MiniProfiler Revisited

While exploring some of the functionality of MiniProfiler there were some 3rd party examples which caught my attention.

using (SqlConnection connection = new SqlConnection(@"Data Source=...; Initial Catalog=SyncDB; Trusted_Connection=Yes"))
{
    using (ProfiledDbConnection profiledDbConnection = new ProfiledDbConnection(connection, MiniProfiler.Current))
    {
    if (profiledDbConnection.State != System.Data.ConnectionState.Open)
        profiledDbConnection.Open();
    using (SqlCommand command = new SqlCommand("Select * From Authors", connection))
    {
        using (ProfiledDbCommand profiledDbCommand = new ProfiledDbCommand(command, connection, MiniProfiler.Current))
        {                               
            var data = profiledDbCommand.ExecuteReader();

            //Write code here to populate the list of Authors
        }
    }
}                  

“Inspired” by code like this my first attempt to retrieve a list of stock items didn’t look right.

[HttpGet("AdoProfiledOtt")]
public async Task<ActionResult<IEnumerable<Model.StockItemListDtoV1>>> GetAdoProfiledOtt()
{
    List<Model.StockItemListDtoV1> response = new List<Model.StockItemListDtoV1>();

    using (SqlConnection connection = new SqlConnection(configuration.GetConnectionString("default")))
    {
        using (ProfiledDbConnection profiledDbConnection = new ProfiledDbConnection(connection, MiniProfiler.Current))
        {
            await profiledDbConnection.OpenAsync();

            using (SqlCommand command = new SqlCommand(sqlCommandText, connection))
            {
                using (ProfiledDbCommand profiledDbCommand = new ProfiledDbCommand(command, profiledDbConnection, MiniProfiler.Current))
                {
                    using (SqlDataReader reader = await command.ExecuteReaderAsync())
                    {
                        using (ProfiledDbDataReader profiledDbDataReader = new ProfiledDbDataReader(reader, MiniProfiler.Current))
                        {
                            var rowParser = profiledDbDataReader.GetRowParser<Model.StockItemListDtoV1>();

                            while (await profiledDbDataReader.ReadAsync())
                            {
                                response.Add(rowParser(profiledDbDataReader));
                            }
                        }
                    }
                }
            }

            await profiledDbConnection.CloseAsync();
        }
    }
}

Often Dapper has functionality like closing the connection if it needed to open it to reduce the amount of code required and this code looked verbose.

[HttpGet]
public async Task<ActionResult<IEnumerable<Model.StockItemListDtoV1>>> Get()
{
	IEnumerable<Model.StockItemListDtoV1> response = null;

	using (SqlConnection db = new SqlConnection(this.connectionString))
	{
		response = await db.QueryWithRetryAsync<Model.StockItemListDtoV1>(sql: @"SELECT [StockItemID] as ""ID"", [StockItemName] as ""Name"", [RecommendedRetailPrice], [TaxRate] FROM [Warehouse].[StockItems]", commandType: CommandType.Text);
	}
	return this.Ok(response);
}

It seemed a bit odd that so many “usings” were needed so I had a look at ProfiledDBConnection.cs

/// <summary>
/// Initializes a new instance of the <see cref="ProfiledDbConnection"/> class.
/// Returns a new <see cref="ProfiledDbConnection"/> that wraps <paramref name="connection"/>,
/// providing query execution profiling. If profiler is null, no profiling will occur.
/// </summary>
/// <param name="connection"><c>Your provider-specific flavour of connection, e.g. SqlConnection, OracleConnection</c></param>
/// <param name="profiler">The currently started <see cref="MiniProfiler"/> or null.</param>
/// <exception cref="ArgumentNullException">Throws when <paramref name="connection"/> is <c>null</c>.</exception>
public ProfiledDbConnection(DbConnection connection, IDbProfiler? profiler)
{
    _connection = connection ?? throw new ArgumentNullException(nameof(connection));
    _connection.StateChange += StateChangeHandler;

    if (profiler != null)
    {
        _profiler = profiler;
    }
}
...
/// <summary>
/// Dispose the underlying connection.
/// </summary>
/// <param name="disposing">false if preempted from a <c>finalizer</c></param>
protected override void Dispose(bool disposing)
{
    if (disposing && _connection != null)
    {
        _connection.StateChange -= StateChangeHandler;
        _connection.Dispose();
    }
    base.Dispose(disposing);
    _connection = null!;
    _profiler = null;
}

One less “using” required as ProfiledDbConnection “automagically” disposes the SqlConnection. It also seemed a bit odd that the SqlCommand had a “using” so I had a look at ProfiledDbCommand.cs

 /// <summary>
/// Initializes a new instance of the <see cref="ProfiledDbCommand"/> class.
/// </summary>
/// <param name="command">The command.</param>
/// <param name="connection">The connection.</param>
/// <param name="profiler">The profiler.</param>
/// <exception cref="ArgumentNullException">Throws when <paramref name="command"/> is <c>null</c>.</exception>
public ProfiledDbCommand(DbCommand command, DbConnection? connection, IDbProfiler? profiler)
{
    _command = command ?? throw new ArgumentNullException(nameof(command));

    if (connection != null)
    {
        _connection = connection;
        UnwrapAndAssignConnection(connection);
    }

    if (profiler != null)
    {
        _profiler = profiler;
    }
}
...
/// <summary>
/// Releases all resources used by this command.
/// </summary>
/// <param name="disposing">false if this is being disposed in a <c>finalizer</c>.</param>
protected override void Dispose(bool disposing)
{
   if (disposing && _command != null)
   {
       _command.Dispose();
    }
    _command = null!;
    base.Dispose(disposing);
}

Another “using” not required as ProfiledDbCommand “automagically” disposes the SqlCommand as well. It also seemed a bit odd that the SqlDataReader had a using so I had a look at profileDbDataReader.cs

/// <summary>
/// Initializes a new instance of the <see cref="ProfiledDbDataReader"/> class (with <see cref="CommandBehavior.Default"/>).
/// </summary>
/// <param name="reader">The reader.</param>
/// <param name="profiler">The profiler.</param>
public ProfiledDbDataReader(DbDataReader reader, IDbProfiler profiler) : this(reader, CommandBehavior.Default, profiler) { }

/// <summary>
/// Initializes a new instance of the <see cref="ProfiledDbDataReader"/> class.
/// </summary>
/// <param name="reader">The reader.</param>
/// <param name="behavior">The behavior specified during command execution.</param>
/// <param name="profiler">The profiler.</param>
public ProfiledDbDataReader(DbDataReader reader, CommandBehavior behavior, IDbProfiler? profiler)
{
    WrappedReader = reader;
    Behavior = behavior;
    _profiler = profiler;
}
...
/// <summary>
/// The <see cref="DbDataReader"/> that is being used.
/// </summary>
public DbDataReader WrappedReader { get; }

/// <inheritdoc cref="DbDataReader.Dispose(bool)"/>
protected override void Dispose(bool disposing)
{
    // reader can be null when we're not profiling, but we've inherited from ProfiledDbCommand and are returning a
    // an unwrapped reader from the base command
    WrappedReader?.Dispose();
    base.Dispose(disposing);
}

Another “using” not required as ProfiledDbDataReader “automagically” disposes the SqlDataReader. This was my final version of profiling the System.Data.SqlClient code to retrieve a list of stock items.

[HttpGet("AdoProfiled")]
public async Task<ActionResult<IEnumerable<Model.StockItemListDtoV1>>> GetProfiledAdo()
{
    List<Model.StockItemListDtoV1> response = new List<Model.StockItemListDtoV1>();

    using (ProfiledDbConnection profiledDbConnection = new ProfiledDbConnection((SqlConnection)dapperContext.ConnectionCreate(), MiniProfiler.Current))
    {
        await profiledDbConnection.OpenAsync();

        using (ProfiledDbCommand profiledDbCommand = new ProfiledDbCommand(new SqlCommand(sqlCommandText), profiledDbConnection, MiniProfiler.Current))
        {
            DbDataReader reader = await profiledDbCommand.ExecuteReaderAsync();

            using (ProfiledDbDataReader profiledDbDataReader = new ProfiledDbDataReader(reader, MiniProfiler.Current))
            {
                var rowParser = profiledDbDataReader.GetRowParser<Model.StockItemListDtoV1>();

                while (await profiledDbDataReader.ReadAsync())
                {
                    response.Add(rowParser(profiledDbDataReader));
                }
            }
        }
    }

    return this.Ok(response);
}

The profileDbDataReader.cs implementation was “sparse” and when loading a longer list of stock items there were some ReadAsync calls which took a bit longer.

/// <summary>
/// The profiled database data reader.
/// </summary>
public class ProfiledDbDataReader : DbDataReader
{
    private readonly IDbProfiler? _profiler;

    /// <summary>
    /// Initializes a new instance of the <see cref="ProfiledDbDataReader"/> class (with <see cref="CommandBehavior.Default"/>).
    /// </summary>
    /// <param name="reader">The reader.</param>
    /// <param name="profiler">The profiler.</param>
    public ProfiledDbDataReader(DbDataReader reader, IDbProfiler profiler) : this(reader, CommandBehavior.Default, profiler) { }

    /// <summary>
    /// Initializes a new instance of the <see cref="ProfiledDbDataReader"/> class.
    /// </summary>
    /// <param name="reader">The reader.</param>
    /// <param name="behavior">The behavior specified during command execution.</param>
    /// <param name="profiler">The profiler.</param>
    public ProfiledDbDataReader(DbDataReader reader, CommandBehavior behavior, IDbProfiler? profiler)
    {
        WrappedReader = reader;
        Behavior = behavior;
        _profiler = profiler;
    }

    /// <summary>Gets the behavior specified during command execution.</summary>
    public CommandBehavior Behavior { get; }

    /// <inheritdoc cref="DbDataReader.Depth"/>
    public override int Depth => WrappedReader.Depth;

    /// <inheritdoc cref="DbDataReader.FieldCount"/>
    public override int FieldCount => WrappedReader.FieldCount;

    /// <inheritdoc cref="DbDataReader.HasRows"/>
    public override bool HasRows => WrappedReader.HasRows;

    /// <inheritdoc cref="DbDataReader.IsClosed"/>
    public override bool IsClosed => WrappedReader.IsClosed;

    /// <inheritdoc cref="DbDataReader.RecordsAffected"/>
    public override int RecordsAffected => WrappedReader.RecordsAffected;

    /// <summary>
    /// The <see cref="DbDataReader"/> that is being used.
    /// </summary>
    public DbDataReader WrappedReader { get; }

    /// <inheritdoc cref="DbDataReader.this[string]"/>
    public override object this[string name] => WrappedReader[name];

    /// <inheritdoc cref="DbDataReader.this[int]"/>
    public override object this[int ordinal] => WrappedReader[ordinal];
...
    /// <inheritdoc cref="DbDataReader.GetString(int)"/>
    public override string GetString(int ordinal) => WrappedReader.GetString(ordinal);

    /// <inheritdoc cref="DbDataReader.GetValue(int)"/>
    public override object GetValue(int ordinal) => WrappedReader.GetValue(ordinal);

    /// <inheritdoc cref="DbDataReader.GetValues(object[])"/>
    public override int GetValues(object[] values) => WrappedReader.GetValues(values);

    /// <inheritdoc cref="DbDataReader.IsDBNull(int)"/>
    public override bool IsDBNull(int ordinal) => WrappedReader.IsDBNull(ordinal);

    /// <inheritdoc cref="DbDataReader.IsDBNullAsync(int, CancellationToken)"/>
    public override Task<bool> IsDBNullAsync(int ordinal, CancellationToken cancellationToken) => WrappedReader.IsDBNullAsync(ordinal, cancellationToken);

    /// <inheritdoc cref="DbDataReader.NextResult()"/>
    public override bool NextResult() => WrappedReader.NextResult();

    /// <inheritdoc cref="DbDataReader.NextResultAsync(CancellationToken)"/>
    public override Task<bool> NextResultAsync(CancellationToken cancellationToken) => WrappedReader.NextResultAsync(cancellationToken);

    /// <inheritdoc cref="DbDataReader.Read()"/>
    public override bool Read() => WrappedReader.Read();

    /// <inheritdoc cref="DbDataReader.ReadAsync(CancellationToken)"/>
    public override Task<bool> ReadAsync(CancellationToken cancellationToken) => WrappedReader.ReadAsync(cancellationToken);

    /// <inheritdoc cref="DbDataReader.Close()"/>
    public override void Close()
    {
        // reader can be null when we're not profiling, but we've inherited from ProfiledDbCommand and are returning a
        // an unwrapped reader from the base command
        WrappedReader?.Close();
        _profiler?.ReaderFinish(this);
    }

    /// <inheritdoc cref="DbDataReader.GetSchemaTable()"/>
    public override DataTable? GetSchemaTable() => WrappedReader.GetSchemaTable();

    /// <inheritdoc cref="DbDataReader.Dispose(bool)"/>
    protected override void Dispose(bool disposing)
    {
        // reader can be null when we're not profiling, but we've inherited from ProfiledDbCommand and are returning a
        // an unwrapped reader from the base command
        WrappedReader?.Dispose();
        base.Dispose(disposing);
    }
}

In the [HttpGet(“DapperProfiledQueryMultipleStep”)] method I wrapped ReadAsync and could see in the profiling that every so often a call did take significantly longer.

using (MiniProfiler.Current.Step("invoiceSummaryLine.ReadAsync"))
{
   response.InvoiceLines = await invoiceSummary.ReadAsync<Model.InvoiceLineSummaryListDtoV1>();
}

I did consider modifying profileDbDataReader.cs to add some instrumentation to the Read… and Get… methods but, the authors of miniprofiler are way way smarter than me so there must be a reason why they didn’t.

.NET ASP.NET Core Custom Identity Provider Sample Update

As part of some security updates to one of the ASP.NET Core applications I work on there were changes to how users are authorised and authenticated(A&A). As part of this we were looking at replacing custom A&A (login names & passwords+ existing database) with ASP.NET Core Identity and a Custom Storage Provider. I downloaded the sample code from the Github AspNetCore.Docs repository and opened the project in Visual Studio 2022.

The Custom Storage Provider sample hasn’t been updated for years and used .NET Core 1.1 which was released Q4 2016.

When I opened the project there were errors and many of the NuGets were deprecated.

The sample code uses Dapper (micro ORM) to access tables in a custom Microsoft SQL Server database but there was also NuGets and code that referenced Entity Framework Core.

After several unsuccessful attempts at updating the NuGets packages I started again from scratch

The code wouldn’t compile so I started fixing issues (The first couple of attempts were very “hacky”). The UseDatabaseErrorPage method was from EF Core so it was commented out. The UseBrowserLink method was from the Browser Link support which I decided not to use etc.

...
namespace CustomIdentityProviderSample
{
    public class Startup
    {
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

            if (env.IsDevelopment())
            {
                // For more details on using the user secret store see https://go.microsoft.com/fwlink/?LinkID=532709
                builder.AddUserSecrets<Startup>();
            }

            builder.AddEnvironmentVariables();
            Configuration = builder.Build();
        }

        public IConfigurationRoot Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Add identity types
            services.AddIdentity<ApplicationUser, ApplicationRole>()
                .AddDefaultTokenProviders();

            // Identity Services
            services.AddTransient<IUserStore<ApplicationUser>, CustomUserStore>();
            services.AddTransient<IRoleStore<ApplicationRole>, CustomRoleStore>();
            string connectionString = Configuration.GetConnectionString("DefaultConnection");
            services.AddTransient<SqlConnection>(e => new SqlConnection(connectionString));
            services.AddTransient<DapperUsersTable>();

            services.AddMvc();

            // Add application services.
            services.AddTransient<IEmailSender, AuthMessageSender>();
            services.AddTransient<ISmsSender, AuthMessageSender>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            // loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            // loggerFactory.AddDebug();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                // app.UseDatabaseErrorPage(); BHL
                // app.UseBrowserLink(); BHL
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseStaticFiles();

            app.UseRouting(); // BHL

            // app.UseIdentity(); BHL
            app.UseAuthentication();
            app.UseAuthorization();

            // Add external authentication middleware below. To configure them please see https://go.microsoft.com/fwlink/?LinkID=532715
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

This process was repeated many times until the Custom Storage Provider sample compiled so I could run it in the Visual Studio 2022 debugger.

The Custom Storage Provider sample then failed because of changes to routing etc.

The Custom Storage Provider sample then failed because of changes to A&A middleware

The Custom Storage Provider sample then failed because the database creation script and the code didn’t match.

CREATE TABLE [dbo].[CustomUser](
	[Id] [uniqueidentifier] NOT NULL,
	[Email] [nvarchar](256) NULL,
	[EmailConfirmed] [bit] NOT NULL,
	[PasswordHash] [nvarchar](max) NULL,
	[UserName] [nvarchar](256) NOT NULL,
 CONSTRAINT [PK_dbo.CustomUsers] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

Finally, the Custom Storage Provider sample worked but the page layout was broken

I then worked through the Razor views adding stylesheets where necessary.

@inject Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet JavaScriptSnippet
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - CustomIdentityProviderSample</title>
    @* BHL *@
    <link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.css"
          asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.css"
          asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" />

    <environment names="Development">
        <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    </environment>
    <environment names="Staging,Production">
        <link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.css"
              asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.css"
              asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" />
    </environment>
    @Html.Raw(JavaScriptSnippet.FullScript)
</head>
...

The Custom Storage Provider sample subset of pages then rendered better.

The Custom Storage Provider sample then failed when functionality like user phone number was used.

This required the addition of implementations (many just throw NotImplementedException exceptions) for IUserPhoneNumberStore, IUserTwoFactorStore, and IUserLoginStore etc.

using Microsoft.AspNetCore.Identity;
using System;
using System.Threading.Tasks;
using System.Threading;
using System.Collections.Generic;

namespace CustomIdentityProviderSample.CustomProvider
{
    /// <summary>
    /// This store is only partially implemented. It supports user creation and find methods.
    /// </summary>
    public class CustomUserStore : IUserStore<ApplicationUser>, 
        IUserPasswordStore<ApplicationUser>,
        IUserPhoneNumberStore<ApplicationUser>,
        IUserTwoFactorStore<ApplicationUser>,
        IUserLoginStore<ApplicationUser>
    {
        private readonly DapperUsersTable _usersTable;

        public CustomUserStore(DapperUsersTable usersTable)
        {
            _usersTable = usersTable;
        }

        public Task AddLoginAsync(ApplicationUser user, UserLoginInfo login, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public async Task<IdentityResult> CreateAsync(ApplicationUser user, 
            CancellationToken cancellationToken = default(CancellationToken))
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (user == null) throw new ArgumentNullException(nameof(user));

            return await _usersTable.CreateAsync(user);
        }

        public async Task<IdentityResult> DeleteAsync(ApplicationUser user, 
            CancellationToken cancellationToken = default(CancellationToken))
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (user == null) throw new ArgumentNullException(nameof(user));

            return await _usersTable.DeleteAsync(user);

        }

        public async Task<ApplicationUser> FindByIdAsync(string userId, 
            CancellationToken cancellationToken = default(CancellationToken))
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (userId == null) throw new ArgumentNullException(nameof(userId));
            Guid idGuid;
            if(!Guid.TryParse(userId, out idGuid))
            {
                throw new ArgumentException("Not a valid Guid id", nameof(userId));
            }

            return await _usersTable.FindByIdAsync(idGuid);

        }

        public Task<ApplicationUser> FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public async Task<ApplicationUser> FindByNameAsync(string userName, 
            CancellationToken cancellationToken = default(CancellationToken))
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (userName == null) throw new ArgumentNullException(nameof(userName));

            return await _usersTable.FindByNameAsync(userName);
        }

        public async Task<IList<UserLoginInfo>> GetLoginsAsync(ApplicationUser user, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (user == null) throw new ArgumentNullException(nameof(user));

            return await _usersTable.GetLoginsAsync(user.Id);
        }
... 
}

This also required extensions to the DapperUsersTable.cs.

public async Task<IdentityResult> UpdateAsync(ApplicationUser user)
{
   string sql = "UPDATE dbo.AspNetUsers " + // BHL
                     "SET [Id] = @Id, [Email]= @Email, [EmailConfirmed] = @EmailConfirmed, [PasswordHash] = @PasswordHash, [UserName] = @UserName " +
                     "WHERE Id = @Id;";


   int rows = await _connection.ExecuteAsync(sql, new { user.Id, user.Email, user.EmailConfirmed, user.PasswordHash, user.UserName });

   if (rows == 1)
   {
      return IdentityResult.Success;
   }

return IdentityResult.Failed(new IdentityError { Description = $"Could not update user {user.Email}." });
}

After many failed attempts my very nasty Custom Storage Provider refresh works (with many warnings and messages). I now understand how they work well enough that I am going to start again from scratch.