Example Code - Opinionated ContosoUniversity on ASP.NET Core 2.0's Razor Pages
The best way to learn about code isn't just writing more code - it's reading code! Not all of it will be great code and much of it won't be the way you would do it, but it's a great way to expand your horizons.
In fact, I'd argue that most people aren't reading enough code. Perhaps there's not enough clean code bases to check out and learn from.
I was pleased to stumble on this code base from Jimmy Bogard called Contoso University at https://github.com/jbogard/ContosoUniversityDotNetCore-Pages.
There's a LOT of good stuff to read in this repo so I won't claim to have read it all or as deeply as I could. In fact, there's a good solid day of reading and absorbing here.However, here's some of the things I noticed and that I appreciate. Some of this is very "Jimmy" code, since it was written for and by Jimmy. This is a good thing and not a dig. We all collect patterns and make libraries and develop our own spins on architectural styles. I love that Jimmy collects a bunch of things he's created or contributed to over the years and put it into a nice clear sample for us to read. As Jimmy points out, there's a lot in https://github.com/jbogard/ContosoUniversityDotNetCore-Pages to explore:
- CQRS pattern and MediatR
- AutoMapper for automatically mapping "left hand/right hand" objects
- Vertical slice architecture
- Razor Pages
- Fluent Validation and Shouldly
- HtmlTags object model for generating HTML
- Entity Framework Core
Clone and Build just works
A low bar, right? You'd be surprised how often I git clone someone's repository and they haven't tested it elsewhere. Bonus points for a build.ps1 that bootstraps whatever needs to be done. I had .NET Core 2.x on my system already and this build.ps1 got the packages I needed and built the code cleanly.
It's an opinioned project with some opinions. ;) And that's great, because it means I'll learn about techniques and tools that I may not have used before. If someone uses a tool that's not the "defaults" it may me that the defaults are lacking!
- Build.ps1 is using a build script style taken from PSake, a powershell build automation tool.
- It's building to a folder called ./artifacts as as convention.
- Inside build.ps1, it's using Roundhouse, a Database Migration Utility for .NET using sql files and versioning based on source control http://projectroundhouse.org
- It's set up for Continuous Integration in AppVeyor, a lovely CI/CD system I use myself.
- It uses the Octo.exe tool from OctopusDeploy to package up the artifacts.
Organized and Easy to Read
I'm finding the code easy to read for the most part. I started at Startup.cs to just get a sense of what middleware is being brought in.
public void ConfigureServices(IServiceCollection services) { services.AddMiniProfiler().AddEntityFramework(); services.AddDbContext<SchoolContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); services.AddAutoMapper(typeof(Startup)); services.AddMediatR(typeof(Startup)); services.AddHtmlTags(new TagConventions()); services.AddMvc(opt => { opt.Filters.Add(typeof(DbContextTransactionPageFilter)); opt.Filters.Add(typeof(ValidatorPageFilter)); opt.ModelBinderProviders.Insert(0, new EntityModelBinderProvider()); }) .SetCompatibilityVersion(CompatibilityVersion.Version_2_1) .AddFluentValidation(cfg => { cfg.RegisterValidatorsFromAssemblyContaining<Startup>(); }); }Here I can see what libraries and helpers are being brought in, like AutoMapper, MediatR, and HtmlTags. Then I can go follow up and learn about each one.
MiniProfiler
I've always loved MiniProfiler. It's a hidden gem of .NET and it's been around being awesome forever. I blogged about it back in 2011! It sits in the corner of your web page and gives you REAL actionable details on how your site behaves and what the important perf timings are.
It's even better with EF Core in that it'll show you the generated SQL as well! Again, all inline in your web site as you develop it.
Very nice.
Clean Unit Tests
Jimmy is using XUnit and has an IntegrationTestBase here with some stuff I don't understand, like SliceFixture. I'm marking this as something I need to read up on and research. I can't tell if this is the start of a new testing helper library, as it feels too generic and important to be in this sample.
He's using the CQRS "Command Query Responsibility Segregation" pattern. Here starts with a Create command, sends it, then does a Query to confirm the results. It's very clean and he's got a very isolated test.
[Fact] public async Task Should_get_edit_details() { var cmd = new Create.Command { FirstMidName = "Joe", LastName = "Schmoe", EnrollmentDate = DateTime.Today }; var studentId = await SendAsync(cmd); var query = new Edit.Query { Id = studentId }; var result = await SendAsync(query); result.FirstMidName.ShouldBe(cmd.FirstMidName); result.LastName.ShouldBe(cmd.LastName); result.EnrollmentDate.ShouldBe(cmd.EnrollmentDate); }
FluentValidator
https://fluentvalidation.net is a helper library for creating clear strongly-typed validation rules. Jimmy uses it throughout and it makes for very clean validation code.
public class Validator : AbstractValidator<Command> { public Validator() { RuleFor(m => m.Name).NotNull().Length(3, 50); RuleFor(m => m.Budget).NotNull(); RuleFor(m => m.StartDate).NotNull(); RuleFor(m => m.Administrator).NotNull(); } }
Useful Extensions
Looking at a project's C# extension methods is a great way to determine what the author feels are gaps in the underlying included functionality. These are useful for returning JSON from Razor Pages!
public static class PageModelExtensions { public static ActionResult RedirectToPageJson<TPage>(this TPage controller, string pageName) where TPage : PageModel { return controller.JsonNet(new { redirect = controller.Url.Page(pageName) } ); } public static ContentResult JsonNet(this PageModel controller, object model) { var serialized = JsonConvert.SerializeObject(model, new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore }); return new ContentResult { Content = serialized, ContentType = "application/json" }; } }
PaginatedList
I've always wondered what to do with helper classes like PaginatedList. Too small for a package, too specific to be built-in? What do you think?
public class PaginatedList<T> : List<T> { public int PageIndex { get; private set; } public int TotalPages { get; private set; } public PaginatedList(List<T> items, int count, int pageIndex, int pageSize) { PageIndex = pageIndex; TotalPages = (int)Math.Ceiling(count / (double)pageSize); this.AddRange(items); } public bool HasPreviousPage { get { return (PageIndex > 1); } } public bool HasNextPage { get { return (PageIndex < TotalPages); } } public static async Task<PaginatedList<T>> CreateAsync(IQueryable<T> source, int pageIndex, int pageSize) { var count = await source.CountAsync(); var items = await source.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(); return new PaginatedList<T>(items, count, pageIndex, pageSize); } }
I'm still reading all the source I can. Absorbing what resonates with me, considering what I don't know or understand and creating a queue of topics to read about. I'd encourage you to do the same! Thanks Jimmy for writing this large sample and for giving us some code to read and learn from!
Sponsor: Scale your Python for big data & big science with Intel® Distribution for Python. Near-native code speed. Use with NumPy, SciPy & scikit-learn. Get it Today!
About Scott
Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.
About Newsletter
Although I am not sure about the example starting a transaction - even on the loading of a view.
I have also been working through Piotr Gankiewicz's DShop example for microservices (https://github.com/devmentors?tab=repositories) which uses .NET Core 2.1, RabbitMQ, MongoDB, Docker, Redis, etc. which he is blogging about here: http://piotrgankiewicz.com/blog/
the SliceFixture is a helper class to avoid code duplication and has a key feature in it: reset the database to a clean checkpoint before each test in order to have reliable tests (https://lostechies.com/jimmybogard/2015/02/19/reliable-database-tests-with-respawn/). To accomplish this task, Jimmy has developed a great utility: Respwan (https://github.com/jbogard/Respawn). I think its worth to be mentioned in your post as well. It is essential in the integration tests that use the database!
We been using MediatR with CQRS pattern and its just awesome. But I have to make a point in how he is doing it here (command and handler in the same file as code behind). Instead, we have the following structure:
/Services
/Commands
/Queries
/Validators
/Controllers
/Api
/Web
In Commands and Queries, we have the Request, the Response and the Handler. So we can use it either from Api controllers (configured with Swashbuckle) and the Web Controllers (for Asp.Net Core MVC) or even between them (not recommended). This structure provides the perfect isolation. Of course, it has a lot more but I wanted a concise example.
We have a similar structure for SignalR with DDD, where the Request and Response are declared within Services (Application Layer) but the handler is at the Site (Distributed Service) for accesing the SignalR Hub.
Thank you
Comments are closed.
It's on my todo list to fork and make it a bit more friendly to clone and run on non windows environments.
For example on the mac even with Powershell installed I get the following.
Moar cross platform samples :)