Skip to content

Instantly share code, notes, and snippets.

@jongalloway
Last active April 30, 2022 08:24
Show Gist options
  • Star 31 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save jongalloway/70e5373837534abe6c89e7ab3ec4efb5 to your computer and use it in GitHub Desktop.
Save jongalloway/70e5373837534abe6c89e7ab3ec4efb5 to your computer and use it in GitHub Desktop.
ASP.NET Core One Hour Makeover

https://aka.ms/aspnetcore-makeover

Outline

  • Intro (goals/non-goals/what you'll learn)
  • Pick The Right Starting Template
    • Web Application (no auth)
    • Web API
    • SPA
    • Other templates
  • Source Control and Solution Structure
    • editorconfig
    • gitignore
  • Maintainability
    • Tests
    • Health Checks
  • Front End
  • Performance (no way we'll get to this in 50 minutes...)
    • Caching
    • Miniprofiler
    • Web Optimizer
  • Creating your own templates

Code Snippets

FunctionalTests.cs

using Microsoft.AspNetCore.Mvc.Testing;
using OneHour.Web;
using System;
using System.Threading.Tasks;
using Xunit;

namespace WebApp.Tests
{
	public class BasicTests
		: IClassFixture<WebApplicationFactory<Startup>>
	{
		private readonly WebApplicationFactory<Startup> _factory;

		public BasicTests(WebApplicationFactory<Startup> factory)
		{
			_factory = factory;
		}

		[Theory]
		[InlineData("/")]
		[InlineData("/Index")]
		[InlineData("/Privacy")]
		public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
		{
			// Arrange
			var client = _factory.CreateClient();

			// Act
			var response = await client.GetAsync(url);

			// Assert
			response.EnsureSuccessStatusCode(); // Status Code 200-299
			Assert.Equal("text/html; charset=utf-8",
				response.Content.Headers.ContentType.ToString());
		}

		[Fact]
		public async Task Get_HealthCheckReturnsHealthy()
		{
			// Arrange
			var client = _factory.CreateClient();

			// Act
			var response = await client.GetAsync("/health");

			// Assert
			response.EnsureSuccessStatusCode(); // Status Code 200-299
			Assert.Equal("Healthy",
				response.Content.ReadAsStringAsync().Result);

		}
	}
}

libman.json

{
"version": "1.0",
"defaultProvider": "cdnjs",
"libraries": [
  {
    "library": "jquery@3.4.1",
    "files": [
      "jquery.min.js"
    ],
    "destination": "wwwroot/lib/jquery/dist"
  },
  {
    "library": "jquery-validation-unobtrusive@3.2.11",
    "files": [
      "jquery.validate.unobtrusive.min.js"
    ],
    "destination": "wwwroot/lib/jquery-validation-unobtrusive/"
  },
  {
    "library": "jquery-validation@1.19.1",
    "provider": "jsdelivr",
    "files": [
      "dist/jquery.validate.js",
      "dist/additional-methods.js"
    ],
    "destination": "wwwroot/lib/jquery-validation/dist/"
  },
  {
    "provider": "unpkg",
    "library": "bootstrap@4.1.3",
    "destination": "wwwroot/lib/bootstrap/",
    "files": [
      "dist/css/bootstrap.css",
      "dist/css/bootstrap-grid.css",
      "dist/css/bootstrap-reboot.css",
      "dist/js/bootstrap.js"
    ]
  },
  {
    "provider": "unpkg",
    "library": "ionicons@4.5.5",
    "destination": "wwwroot/lib/ionicons",
    "files": [ "dist/css/ionicons.min.css" ]
  }
]
}  

Sitemap Extension

using Ducksoft.NetCore.Razor.Sitemap.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;

namespace OneHour.Web.Utility
{
	public static class SitemapExtensions
	{
		public static IServiceCollection ConfigureMvcRazorPages(this IServiceCollection services,
			  CompatibilityVersion version, string startPageUrl = "", string startPageArea = "")
		{
			services.AddMvc()
				.SetCompatibilityVersion(version)
				.AddRazorPagesOptions(options =>
				{
					var isSupportAreas = !string.IsNullOrWhiteSpace(startPageArea);
					options.AllowAreas = isSupportAreas;
					options.AllowMappingHeadRequestsToGetHandler = true;
					if (isSupportAreas)
					{
						options.Conventions.AddAreaPageRoute(startPageArea, startPageUrl, string.Empty);
					}
					else if (!string.IsNullOrWhiteSpace(startPageUrl))
					{
						options.Conventions.AddPageRoute(startPageUrl, string.Empty);
					}
				})
				.AddRazorPagesOptions(options =>
				{
					options.Conventions.Add(new SitemapRouteConvention());
				})
				.AddRazorPagesOptions(options =>
				{
					options.Conventions.AddPageRoute("/Sitemap", "sitemap.xml");
				});

			return services;
		}
	}
}

.template.config/template.json

{
"$schema": "http://json.schemastore.org/template",
"author": "YOUR NAME",
"classifications": [ "ASP.NET Core", "Solution" ],
"identity": "[YOUR NAME].AspNetCoreSolutionTemplateTemplate.CSharp",
"name": "ASP.NET Core One Hour",
"shortName": "aspnetonehour",
"sourceName": "OneHour"
}

https://aka.ms/aspnetcore-makeover

Outline

  • Intro (goals/non-goals/what you'll learn)
  • Pick The Right Starting Template
    • Web Application (no auth)
    • Web API
    • SPA
    • Other templates (teaser)
  • Source Control and Solution Structure
    • editorconfig
    • gitignore
  • Maintainability
    • Tests
    • Health Checks
    • Debug Footer
  • Front End
    • Bootstrap
    • Front end build?
    • Libman
    • SEO
  • Performance (no way we'll get to this in 50 minutes...)
    • ANCM
    • Tiered Compilation
    • Language feature opt-in
    • Caching
    • Miniprofiler
  • Creating your own templates

Code Snippets

FunctionalTests.cs

using Microsoft.AspNetCore.Mvc.Testing;
using OneHour.Web;
using System;
using System.Threading.Tasks;
using Xunit;

namespace WebApp.Tests
{
	public class BasicTests
		: IClassFixture<WebApplicationFactory<Startup>>
	{
		private readonly WebApplicationFactory<Startup> _factory;

		public BasicTests(WebApplicationFactory<Startup> factory)
		{
			_factory = factory;
		}

		[Theory]
		[InlineData("/")]
		[InlineData("/Index")]
		[InlineData("/Privacy")]
		public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
		{
			// Arrange
			var client = _factory.CreateClient();

			// Act
			var response = await client.GetAsync(url);

			// Assert
			response.EnsureSuccessStatusCode(); // Status Code 200-299
			Assert.Equal("text/html; charset=utf-8",
				response.Content.Headers.ContentType.ToString());
		}

		[Fact]
		public async Task Get_HealthCheckReturnsHealthy()
		{
			// Arrange
			var client = _factory.CreateClient();

			// Act
			var response = await client.GetAsync("/health");

			// Assert
			response.EnsureSuccessStatusCode(); // Status Code 200-299
			Assert.Equal("Healthy",
				response.Content.ReadAsStringAsync().Result);

		}
	}
}

libman.json

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
  {
   "library": "jquery@3.3.1",
   "files": [
    "jquery.min.js",
    "jquery.js",
    "jquery.min.map"
   ],
   "destination": "wwwroot/lib/jquery/dist"
  },
  {
   "provider": "unpkg",
   "library": "bootstrap@4.1.3",
   "destination": "wwwroot/lib/bootstrap/",
   "files": [
    "dist/css/bootstrap.css",
    "dist/css/bootstrap-grid.css",
    "dist/css/bootstrap-reboot.css",
    "dist/js/bootstrap.js"
   ]
  },
  {
   "provider": "unpkg",
   "library": "ionicons@4.5.5",
   "destination": "wwwroot/lib/ionicons",
   "files": [ "dist/css/ionicons.min.css" ]
  }
  ]
}  

Sitemap Extension

using Ducksoft.NetCore.Razor.Sitemap.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;

namespace OneHour.Web.Utility
{
	public static class SitemapExtensions
	{
		public static IServiceCollection ConfigureMvcRazorPages(this IServiceCollection services,
			  CompatibilityVersion version, string startPageUrl = "", string startPageArea = "")
		{
			services.AddMvc()
				.SetCompatibilityVersion(version)
				.AddRazorPagesOptions(options =>
				{
					var isSupportAreas = !string.IsNullOrWhiteSpace(startPageArea);
					options.AllowAreas = isSupportAreas;
					options.AllowMappingHeadRequestsToGetHandler = true;
					if (isSupportAreas)
					{
						options.Conventions.AddAreaPageRoute(startPageArea, startPageUrl, string.Empty);
					}
					else if (!string.IsNullOrWhiteSpace(startPageUrl))
					{
						options.Conventions.AddPageRoute(startPageUrl, string.Empty);
					}
				})
				.AddRazorPagesOptions(options =>
				{
					options.Conventions.Add(new SitemapRouteConvention());
				})
				.AddRazorPagesOptions(options =>
				{
					options.Conventions.AddPageRoute("/Sitemap", "sitemap.xml");
				});

			return services;
		}
	}
}

.template.config/template.json

{
"$schema": "http://json.schemastore.org/template",
"author": "YOUR NAME",
"classifications": [ "ASP.NET Core", "Solution" ],
"identity": "[YOUR NAME].AspNetCoreSolutionTemplateTemplate.CSharp",
"name": "ASP.NET Core One Hour",
"shortName": "aspnetonehour",
"sourceName": "OneHour"
}

References and Other Good Stuff

An Opinionated Approach to ASP.NET Core (Scott Allen)

Grab Bag

Existing Template Solutions

Stuff I Wish I'd Had Time For

Outline

  • Intro (goals/non-goals/what you'll learn)
  • Pick The Right Starting Template
    • Web Application (no auth)
    • Web API
    • SPA
    • Other templates (teaser)
  • Front End
    • Bootstrap
    • Front end build?
    • Libman
    • SEO
  • Maintainability
    • Tests
    • Health Checks
    • Debug Footer
  • Source Control and Solution Structure
    • editorconfig
    • gitignore
  • Database (? - probably no)
    • Data seeding?
    • Migrations?
  • Security?
    • Set up Roles (IsAdmin)
  • Performance
    • ANCM
    • Tiered Compilation
    • Language feature opt-in
    • Caching
    • Miniprofiler

Code Snippets

libman.json

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
  {
   "library": "jquery@3.3.1",
   "files": [
    "jquery.min.js",
    "jquery.js",
    "jquery.min.map"
   ],
   "destination": "wwwroot/lib/jquery/dist"
  },
  {
   "provider": "unpkg",
   "library": "bootstrap@4.1.3",
   "destination": "wwwroot/lib/bootstrap/",
   "files": [
    "dist/css/bootstrap.css",
    "dist/css/bootstrap-grid.css",
    "dist/css/bootstrap-reboot.css",
    "dist/js/bootstrap.js"
   ]
  },
  {
   "provider": "unpkg",
   "library": "ionicons@4.5.5",
   "destination": "wwwroot/lib/ionicons",
   "files": [ "dist/css/ionicons.min.css" ]
  }
  ]
}  

References and Other Good Stuff

An Opinionated Approach to ASP.NET Core (Scott Allen)

Grab Bag

Existing Template Solutions

Stuff I Wish I'd Had Time For

Session Video

ASP.NET Core: The One Hour Makeover - Jon Galloway (YouTube)

Checklist

Integration Testing

Health Checks

API

Hosting Optimizations

So you want a front end build...

Next Steps

General Web Stuff

Web Developer Checklist

Other Opinionated Lists / Templates

Outline

  • Intro (goals/non-goals/what you'll learn)
  • Pick The Right Starting Template
    • Web Application (no auth)
    • Web API
    • SPA
    • Other templates (teaser)
  • Front End
    • Bootstrap
    • Front end build?
    • Libman
    • SEO
  • Maintainability
    • Tests
    • Health Checks
    • Debug Footer
  • Source Control and Solution Structure
    • editorconfig
    • gitignore
  • Database (? - probably no)
    • Data seeding?
    • Migrations?
  • Security?
    • Set up Roles (IsAdmin)
  • Performance
    • ANCM
    • Tiered Compilation
    • Language feature opt-in
    • Caching
    • Miniprofiler

Code Snippets

libman.json

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
  {
   "library": "jquery@3.3.1",
   "files": [
    "jquery.min.js",
    "jquery.js",
    "jquery.min.map"
   ],
   "destination": "wwwroot/lib/jquery/dist"
  },
  {
   "provider": "unpkg",
   "library": "bootstrap@4.1.3",
   "destination": "wwwroot/lib/bootstrap/",
   "files": [
    "dist/css/bootstrap.css",
    "dist/css/bootstrap-grid.css",
    "dist/css/bootstrap-reboot.css",
    "dist/js/bootstrap.js"
   ]
  },
  {
   "provider": "unpkg",
   "library": "ionicons@4.5.5",
   "destination": "wwwroot/lib/ionicons",
   "files": [ "dist/css/ionicons.min.css" ]
  }
  ]
}  

template.json

{
"$schema": "http://json.schemastore.org/template",
"author": "YOUR NAME",
"classifications": [ "ASP.NET Core", "Solution" ],
"identity": "[YOUR NAME].AspNetCoreSolutionTemplateTemplate.CSharp",
"name": "ASP.NET Core One Hour",
"shortName": "aspnetonehour",
"sourceName": "OneHour"
}

Sitemap Extension

using Ducksoft.NetCore.Razor.Sitemap.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;

namespace OneHour.Web.Utility
{
	public static class SitemapExtensions
	{
		public static IServiceCollection ConfigureMvcRazorPages(this IServiceCollection services,
			  CompatibilityVersion version, string startPageUrl = "", string startPageArea = "")
		{
			services.AddMvc()
				.SetCompatibilityVersion(version)
				.AddRazorPagesOptions(options =>
				{
					var isSupportAreas = !string.IsNullOrWhiteSpace(startPageArea);
					options.AllowAreas = isSupportAreas;
					options.AllowMappingHeadRequestsToGetHandler = true;
					if (isSupportAreas)
					{
						options.Conventions.AddAreaPageRoute(startPageArea, startPageUrl, string.Empty);
					}
					else if (!string.IsNullOrWhiteSpace(startPageUrl))
					{
						options.Conventions.AddPageRoute(startPageUrl, string.Empty);
					}
				})
				.AddRazorPagesOptions(options =>
				{
					options.Conventions.Add(new SitemapRouteConvention());
				})
				.AddRazorPagesOptions(options =>
				{
					options.Conventions.AddPageRoute("/Sitemap", "sitemap.xml");
				});

			return services;
		}
	}
}

FunctionalTests.cs

using Microsoft.AspNetCore.Mvc.Testing;
using OneHour.Web;
using System;
using System.Threading.Tasks;
using Xunit;

namespace WebApp.Tests
{
	public class BasicTests
		: IClassFixture<WebApplicationFactory<Startup>>
	{
		private readonly WebApplicationFactory<Startup> _factory;

		public BasicTests(WebApplicationFactory<Startup> factory)
		{
			_factory = factory;
		}

		[Theory]
		[InlineData("/")]
		[InlineData("/Index")]
		[InlineData("/Privacy")]
		public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
		{
			// Arrange
			var client = _factory.CreateClient();

			// Act
			var response = await client.GetAsync(url);

			// Assert
			response.EnsureSuccessStatusCode(); // Status Code 200-299
			Assert.Equal("text/html; charset=utf-8",
				response.Content.Headers.ContentType.ToString());
		}

		[Fact]
		public async Task Get_HealthCheckReturnsHealthy()
		{
			// Arrange
			var client = _factory.CreateClient();

			// Act
			var response = await client.GetAsync("/health");

			// Assert
			response.EnsureSuccessStatusCode(); // Status Code 200-299
			Assert.Equal("Healthy",
				response.Content.ReadAsStringAsync().Result);

		}
	}
}

References and Other Good Stuff

An Opinionated Approach to ASP.NET Core (Scott Allen)

Grab Bag

Existing Template Solutions

Stuff I Wish I'd Had Time For

@solrevdev
Copy link

The link for MiniProfiler.AspNetCore.Mvc points to

https://github.com/madskristensen/

But should I think link to

https://miniprofiler.com/dotnet/AspDotNetCore

Found this via your YouTube talk of same name by the way. Great work

@ffes
Copy link

ffes commented Dec 30, 2018

One thing I really miss in the list is adding an EditorConfig file to the solution. It makes collaboration much easier when it comes to various coding styles like spaces vs tabs, ordering of using or the use of var in C#.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment