Claims Authorization Example

Startup.cs

using System.IO;
using System.Linq;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Newtonsoft.Json;

namespace WebApplication25
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            var cookieAuthSection = Configuration.GetSection("CookieAuth");
            string appName = cookieAuthSection.GetValue<string>("ApplicationName");
            string keyLocation = cookieAuthSection.GetValue<string>("KeyLocation");
            services.AddDataProtection()
                .SetApplicationName(appName)
                //.SetDefaultKeyLifetime(TimeSpan.FromDays(9999))
                .PersistKeysToFileSystem(new DirectoryInfo(keyLocation));

            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, config =>
                {
                    config.Cookie.Name = "myapp";
                    config.LoginPath = "/Login";
                    config.AccessDeniedPath = "/AccessDenied";
                });

            services.AddAuthorization(config =>
            {
                config.AddPolicy("UserName", policyBuilder =>
                {
                    policyBuilder.RequireClaim(ClaimTypes.Name);
                });
                config.AddPolicy("Admin", policyBuilder =>
                {
                    policyBuilder.RequireClaim("IsAdmin");
                });
                config.AddPolicy("PhoenixAdmin", policyBuilder =>
                {
                    policyBuilder.RequireClaim("CityAdmin", "Phoenix");
                });
                config.AddPolicy("Zero", policyBuilder =>
                {
                    policyBuilder.RequireAssertion(context =>
                    {
                        var roleClaim = context.User.Claims.FirstOrDefault(x => x.Type == "Roles");
                        if (roleClaim == null) { return false; }

                        var roles = JsonConvert.DeserializeAnonymousType(roleClaim.Value, new[] { new { RoleCode = "", OrgID = 0 } });
                        return roles.Any(r => r.OrgID == 0);
                    });
                });
                config.AddPolicy("AdminOrCityAdmin", policyBuilder =>
                {
                    policyBuilder.RequireAssertion(context =>
                    {
                        return context.User.Claims.Where(x => x.Type == "IsAdmin" || x.Type == "CityAdmin").Any();
                    });
                });
            });

            services.AddRazorPages();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
            }

            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
        }
    }
}

appsettings.json

{
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Information"
        }
    },
    "AllowedHosts": "*",
    "CookieAuth": {
        "ApplicationName": "MyApplication",
        "KeyLocation": "\\\\some-server\\some-share\\keys"
    }
}

_Layout.cshtml

@using System.Security.Claims

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - WebApplication25</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand" asp-area="" asp-page="/Index">WebApplication25</a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/UserAccount">User Account</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Admin">Admin</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/PhoenixAdmin">Phoenix Admin</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Zero">Zero</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/AdminOrCityAdmin">Admin Or City Admin</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-page="/Reset">Reset</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <div>
        Machine Name: @Environment.MachineName
    </div>

    @{
        var claimsIdentity = User?.Identity as ClaimsIdentity;
        if (claimsIdentity != null)
        {
            <ul>
                @foreach (var claim in claimsIdentity.Claims)
                {
                    <li>@claim.Type | @claim.Value</li>
                }
            </ul>
        }
    }


    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2020 - WebApplication25 - <a asp-area="" asp-page="/Privacy">Privacy</a>
            Hello @User?.Identity?.Name
        </div>
    </footer>

    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>

    @RenderSection("Scripts", required: false)
</body>
</html>

AccessDenied.cshtml

@page
@model WebApplication25.Pages.AccessDeniedModel
@{
    ViewData["Title"] = "AccessDenied";
}

<h1>AccessDenied</h1>

Login.cshtml

@page
@model WebApplication25.Pages.LoginModel
@{
    ViewData["Title"] = "Login";
}

<h1>Login</h1>

<form method="post">
    <input type="text" name="UserName" />
    <input type="password" name="Password" />
    <button type="submit">Submit</button>
</form>

Login.cshtml.cs

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;

namespace WebApplication25.Pages
{
    public class LoginModel : PageModel
    {
        [BindProperty]
        public string UserName { get; set; }
        [BindProperty]
        public string Password { get; set; }

        public void OnGet()
        {

        }

        public async Task/*<IActionResult>*/ OnPostAsync()
        {
            if (string.IsNullOrWhiteSpace(UserName))
            {
                throw new ApplicationException("UserName is required");
            }
            var claims = new List<Claim> { new Claim(ClaimTypes.Name, UserName) };

            if (UserName == "admin") { claims.Add(new Claim("IsAdmin", "1")); }

            if (UserName == "phoenix") { claims.Add(new Claim("CityAdmin", "Phoenix")); }

            var rand = new Random();
            var roles = Enumerable.Range(0, 100).Select(i => new
            {
                RoleCode = Guid.NewGuid().ToString().Substring(0, 3),
                OrgID = rand.Next(0, 1000)
            }).ToList();
            if (UserName == "zero") { roles.Add(new { RoleCode = "ZZ", OrgID = 0 }); }

            string rolesJSON = JsonConvert.SerializeObject(roles);
            if (UserName == "manyroles") { claims.Add(new Claim("Roles", rolesJSON)); }
            if (UserName == "zero") { claims.Add(new Claim("Roles", rolesJSON)); }

            var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
            var principal = new ClaimsPrincipal(identity);
            await HttpContext.SignInAsync(principal).ConfigureAwait(false);
        }
    }
}

Admin.cshtml

@page
@model WebApplication25.Pages.AdminModel
@{
    ViewData["Title"] = "Admin";
}

<h1>Admin</h1>

Admin.cshtml.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace WebApplication25.Pages
{
    [Authorize(Policy = "Admin")]
    public class AdminModel : PageModel
    {
        public void OnGet()
        {

        }
    }
}

Others

Repeat for AdminOrCityAdmin.cshtml, PhoenixAdmin.cshtml, UserAccount.cshtml, Zero.cshtml, with the appropriate Authorize attributes representing the policies.

Notes

Adding the key location and app name to the Startup file gives you the ability to put the keys in a file share and share them among multiple servers in a web farm. On a single server, those lines are unnecessary, and it uses the default cookie encryption.

There are definitely better ways to do things like roles, but this example shows that you can dump everything into claims, retrieve them, and validate them with attributes, without a whole lot of work.

Prevent recompile in TFS

Explanation

For some bizarre reason, when checking out a file in TFS, your project gets recompiled. This is annoying if you’re in the middle of working on your web application while you do the checkout.

Reference

Thank you to this StackOverflow answer for the details:

Navigate to C:\Users[user]\AppData\Roaming\Microsoft\VisualStudio\11.0\

Delete the app_offline.htm file

Create a directory called app_offline.htm

Visual Studio apparently gets confused and doesn’t know how to screw up your work anymore.

hgignore file for .NET projects

Here’s a hgignore starting point that I used to use back when I used Mercurial for source control:

syntax: glob

*.obj
*.exe
*.pdb
*.user
*.aps
*.pch
*.scc
*.dbmdl
*.vspscc
*.vssscc
*_i.c
*_p.c
*.ncb
*.suo
*.tlb
*.tlh
*.bak
*.cache
*.ilk
*.log
*.lib
*.sbr
[Bb]in
[Dd]ebug*/
obj/
[Rr]elease*/
_ReSharper*/
[Tt]est[Rr]esult*
[Bb]uild[Ll]og.*
*.[Pp]ublish.xml
[Pp]ublish/
_UpgradeReport_Files/
UpgradeLog.XML

Log Entity Framework details on exception

This worked in old versions, haven’t tested yet in Entity Framework Core – not sure if the structure changed.

public override int SaveChanges() {
    var allEntries = ChangeTracker.Entries().ToList();
    var allAddedEntries = allEntries.Where(e => e.State == EntityState.Added).ToList();
    var allModifiedEntries = allEntries.Where(e => e.State == EntityState.Modified).ToList();
    var allDeletedEntries = allEntries.Where(e => e.State == EntityState.Deleted).ToList();
    try {
        int result = base.SaveChanges();
        return result;
    } catch (Exception ex) {

        try {
            var sb = new StringBuilder(Environment.NewLine);
            sb.AppendLine(string.Format("ADDED:{0}", FormatEntities(allAddedEntries)));
            sb.AppendLine(string.Format("MODIFIED:{0}", FormatEntities(allModifiedEntries)));
            sb.AppendLine(string.Format("DELETED:{0}", FormatEntities(allDeletedEntries)));

            // TODO: Do something with the details
        } catch {
            // If the exception logging fails, just give up
        }
        throw;
    }
}

/// <summary>
/// Takes a collection of entities, retrieves the simple properties from them (numbers, strings, etc.),
/// and JSON-formats the results.
/// </summary>
/// <param name="entries">A collection of Entity-Framework entries.</param>
/// <returns>The JSON-formatted entities.</returns>
private static string FormatEntities(IEnumerable<DbEntityEntry> entries) {
    try {
        var sb = new StringBuilder(Environment.NewLine);
        var simplePropertyTypes = new[]
            {
                typeof(byte), typeof(char), typeof(short), typeof(int), typeof(long),
                typeof(bool), typeof(decimal), typeof(float), typeof(double),
                typeof(string), typeof(DateTime), typeof(ushort), typeof(uint), typeof(ulong),
                typeof(byte?), typeof(char?), typeof(short?), typeof(int?), typeof(long?),
                typeof(bool?), typeof(decimal?), typeof(float?), typeof(double?),
                typeof(DateTime?), typeof(ushort?), typeof(uint?), typeof(ulong?)
            };
        var collection = entries.Select(e => e.Entity).ToList();
        foreach (var entity in collection) {
            var entityType = entity.GetType();

            // Removing the dynamically-generated characters from the end of the entity class name.
            string entityTypeName = Regex.Replace(entityType.Name, @"_[0-9A-F]{64}", string.Empty);

            sb.AppendFormat("{0}: ", entityTypeName);
            var dict = new Dictionary<string, object>();
            var simpleProperties = entityType.GetProperties().Where(p => simplePropertyTypes.Contains(p.PropertyType));
            foreach (var prop in simpleProperties) {
                try {
                    if (prop.GetCustomAttributes(typeof(NotMappedAttribute), true).Length == 0) {
                        dict[prop.Name] = prop.GetValue(entity, null);
                    }
                } catch (Exception) {
                    dict[prop.Name] = "[UNKNOWN]";
                }
            }
            sb.AppendLine(JsonConvert.SerializeObject(dict, Formatting.Indented));
        }
        return sb.ToString();
    } catch (Exception ex) {
        return string.Format("Failed to format entities: {0}", ex.Message);
    }
}

Type-safe JSON result in ASP.NET MVC

public abstract class BaseController : Controller {
    protected internal JsonResult<T> Json<T>(T data) {
        return Json(data, null /* contentType */, null /* contentEncoding */, JsonRequestBehavior.DenyGet);
    }
    protected internal JsonResult<T> Json<T>(T data, string contentType) {
        return Json(data, contentType, null /* contentEncoding */, JsonRequestBehavior.DenyGet);
    }
    protected internal virtual JsonResult<T> Json<T>(T data, string contentType, Encoding contentEncoding) {
        return Json(data, contentType, contentEncoding, JsonRequestBehavior.DenyGet);
    }
    protected internal JsonResult<T> Json<T>(T data, JsonRequestBehavior behavior) {
        return Json(data, null /* contentType */, null /* contentEncoding */, behavior);
    }
    protected internal JsonResult<T> Json<T>(T data, string contentType, JsonRequestBehavior behavior) {
        return Json(data, contentType, null /* contentEncoding */, behavior);
    }
    protected internal virtual JsonResult<T> Json<T>(T data, string contentType, Encoding contentEncoding, JsonRequestBehavior behavior) {
        return new JsonResult<T> {
            Data = data,
            ContentType = contentType,
            ContentEncoding = contentEncoding,
            JsonRequestBehavior = behavior
        };
    }
}

public class JsonResult<T> : JsonResult { }
public class Foo { public int FooId { get; set; } }

// Type-safe result, so you can't accidentally return the wrong type.
[HttpPost]
public JsonResult<Foo> GetSomeFoo() {
    return Json(new Foo { FooId = 1 });
}

Read/write large blob to SQL Server from C#

Rather than try to make it happen in one big command, here’s breaking it out into many commands. Whether or not you use the transaction is up to you and your use case – you could always have a “Completed” column and only set that to true after success, if you wanted to skip a transaction.

First, insert a record, leaving the blob column as an empty byte array, making sure you have access to the primary key of the record you just inserted:

using var conn = new SqlConnection(_config.GetConnectionString("MyDB"));
await conn.OpenAsync().ConfigureAwait(false);
using var tran = (SqlTransaction)await conn.BeginTransactionAsync();
int fileID;
using (var comm = conn.CreateCommand())
{
    comm.Transaction = tran;
    comm.CommandText = @"
        insert dbo.Files (FileName, FileContents)
        values (@FileName, 0x);
        select scope_identity();
    ";
    comm.Parameters.AddWithValue("@FileName", fileName);
    fileID = (int)(await comm.ExecuteScalarAsync().ConfigureAwait(false));
}

Then, in the same connection, write the bytes, 8000 at a time, appending the blob column, and commit the transaction once they’re all done. The “set FileContents.Write” function is a little clumsy, but it works:

using var fileStream = System.IO.File.OpenRead(file);
byte[] buffer = new byte[8000];
int count;
while ((count = fileStream.Read(buffer, 0, buffer.Length)) > 0)
{
    byte[] tmp = new byte[count];
    Array.Copy(buffer, 0, tmp, 0, count);
    using var comm = conn.CreateCommand();
    comm.Transaction = tran;
    comm.CommandText = "update dbo.Files set FileContents.write(@ContentChunk, null, null) where FileID = @FileID";
    comm.Parameters.AddWithValue("@FileID", fileID);
    comm.Parameters.AddWithValue("@ContentChunk", tmp);
    await comm.ExecuteNonQueryAsync();
}
await tran.CommitAsync();

To read the data, use the substring function:

private async IAsyncEnumerable<byte[]> ReadFileAsync(int fileID)
{
    int startingByte = 1;
    while (true)
    {
        byte[] bytes;
        using var conn = new SqlConnection(_config.GetConnectionString("MyDB"));
        await conn.OpenAsync().ConfigureAwait(false);
        using var comm = conn.CreateCommand();
        comm.CommandText = @"
            select substring(FileContents, @StartingByte, 8000) [FileContents]
            from dbo.Files
            where FileID = @FileID;
        ";
        comm.Parameters.Add(new SqlParameter("@FileID", SqlDbType.Int) { Value = fileID });
        comm.Parameters.Add(new SqlParameter("@StartingByte", SqlDbType.Int) { Value = startingByte });
        using var rdr = await comm.ExecuteReaderAsync().ConfigureAwait(false);
        if (!await rdr.ReadAsync().ConfigureAwait(false))
        {
            break;
        }
        bytes = (byte[])rdr[0];
        if (bytes == null || bytes.Length == 0)
        {
            break;
        }
        startingByte += bytes.Length;
        yield return bytes;
    }
}


await foreach (var byteArray in ReadImportFileAsync(importBatchID))
{
    fileWriter.Write(byteArray, 0, byteArray.Length);
}

Render ViewComponent as string

Rendering a View to a string is covered here. Turns out there’s a very similar approach to rendering a ViewComponent as a string. Here’s the code, thanks to this comment on the ASP.NET forums:

    public interface IViewRenderService
    {
        Task<string> RenderToStringAsync(string viewName, object model);
    }

    public class ViewRenderService : IViewRenderService
    {
        private readonly IRazorViewEngine _razorViewEngine;
        private readonly ITempDataProvider _tempDataProvider;
        private readonly IServiceProvider _serviceProvider;

        public ViewRenderService(IRazorViewEngine razorViewEngine,
            ITempDataProvider tempDataProvider,
            IServiceProvider serviceProvider)
        {
            _razorViewEngine = razorViewEngine;
            _tempDataProvider = tempDataProvider;
            _serviceProvider = serviceProvider;
        }

        public async Task<string> RenderToStringAsync(string viewName, object model)
        {
            var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider };
            var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());

            using var sw = new StringWriter();
            var viewResult = _razorViewEngine.FindView(actionContext, viewName, false);

            if (viewResult.View == null)
            {
                throw new ArgumentNullException($"{viewName} does not match any available view");
            }

            var viewDictionary = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary())
            {
                Model = model
            };

            var viewContext = new ViewContext(
                actionContext,
                viewResult.View,
                viewDictionary,
                new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
                sw,
                new HtmlHelperOptions()
            );

            await viewResult.View.RenderAsync(viewContext);
            return sw.ToString();
        }
    }

string result = await _viewRenderService.RenderToStringAsync("Shared/Components/Foo/Default", model);

// Don't forget to register the service so you can use constructor injection in your controller:

public void ConfigureServices(IServiceCollection services)
{
    /// ...
    services.AddScoped<IViewRenderService, ViewRenderService>();
}

Is Development in Razor View

I find myself needing to know if I’m in development from inside a Razor view. In the C# code, you can generally use the #DEBUG preprocessor directive, but it’s not quite so easy in Razor.

I’ve used an extension method on HtmlHelper to kind of cheat, but the “correct” way seems to be to inject the host environment and call the IsDevelopment method:

@using Microsoft.Extensions.Hosting
@inject Microsoft.AspNetCore.Hosting.IWebHostEnvironment HostEnvironment

@if (HostEnvironment.IsDevelopment())
{
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true">
}
else
{
    <link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
}

@if (HostEnvironment.IsDevelopment())
{
    <script src="~/js/bundle.js" asp-append-version="true"></script>
}
else
{
    <script src="~/js/bundle.min.js" asp-append-version="true"></script>
}

JSONP with jQuery and ASP.NET

JSONP allows you to execute something similar to AJAX, across domains without worrying about whether the request is across domains, or any special CORS configuration.

It’s not generally a huge deal to set up CORS, but JSONP is guaranteed to work in all browsers and servers without any special handling.

Javascript

function go() {
    jQuery.ajax({
        // url: "/Home/DoSomething", // Regular MVC
        // url: "/Handler1.ashx", // WebForms
        data: {xyz:"World"},
        dataType: "jsonp",
        type: "get"
    }).done(function (result) {
        console.log(result);
    });
}

ASP.NET

// Regular MVC - no special filters, return types, etc. 
public ActionResult DoSomething(string xyz) 
{
    Response.ContentType = "application/json";
    var foo = new { FirstName = "John", LastName = "Doe", Message = "Hello " + xyz };
    Response.Write(string.Format("{0}({1});", Request["callback"], JsonConvert.SerializeObject(foo)));
    return null;
}

// WebForms
public class Handler1 : IHttpHandler 
{
    public void ProcessRequest(HttpContext context)
    {
        context.Response.ContentType = "application/json";
        var foo = new { FirstName = "John", LastName = "Doe", Message = "Hello " + context.Request["xyz"] };
        context.Response.Write(string.Format("{0}({1});", context.Request["callback"], JsonConvert.SerializeObject(foo)));
    }

    public bool IsReusable { get { return false; } }
}