Scott Hanselman

Example Code - Opinionated ContosoUniversity on ASP.NET Core 2.0's Razor Pages

July 25, 2018 Comment on this post [13] Posted in ASP.NET | DotNetCore | Open Source
Sponsored By

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:

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.

MiniProfiler is the profiler you didn't know you needed

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.

inline SQL in MiniProfiler

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.

facebook twitter subscribe
About   Newsletter
Hosting By
Hosted in an Azure App Service
July 25, 2018 13:32
It's a fantastic resource. I've watched some NDC (I think) talks on the pattern.

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.


✝  Projects/github/ContosoUniversityDotNetCore-Pages   master  pwsh
PowerShell v6.0.3
Copyright (c) Microsoft Corporation. All rights reserved.

https://aka.ms/pscore6-docs
Type 'help' to get help.

PS /Users/solrevdev/Projects/github/ContosoUniversityDotNetCore-Pages> ./Build.ps1
The files /d=ContosoUniversityDotNetCore-Pages, /f=ContosoUniversity\App_Data, /s=(LocalDb)\mssqlloca
ldb, and /silent do not exist.
Exec:
At /Users/solrevdev/Projects/github/ContosoUniversityDotNetCore-Pages/Build.ps1:21 char:9
+ throw ("Exec: " + $errorMessage)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Exec: :String) [], RuntimeException
+ FullyQualifiedErrorId : Exec:

PS /Users/solrevdev/Projects/github/ContosoUniversityDotNetCore-Pages>


Moar cross platform samples :)
July 25, 2018 15:11
Regarding your question about PaginatedList. My preference is source code NuGet packages for this type of stuff. In fact, I've published a package for exactly that. I have a single project that contains hundreds of small packages (one for each source code file). However, I think those are a bit out of date now, since the changes made in NuGet around source code packages. I have a new project which is replacing that one, but I haven't migrated all the packages across yet.
July 25, 2018 16:02
I like Jimmys work.

Although I am not sure about the example starting a transaction - even on the loading of a view.
July 25, 2018 16:28
That's a great example repo, thanks for sharing Scott!

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/
July 25, 2018 23:23
Awesome post! I have a similar project for REST APIs - not as complete but a good starting point for API developers who want to follow a similar pattern: https://github.com/schneidenbach/RecessOpinionatedApiInAspNetCore
July 26, 2018 0:27
Always use Jimmy's Contoso repositories to find out "how its done".
July 26, 2018 10:09
Jimmy Rocks!!!
July 26, 2018 15:21
Hi Scott,

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!
July 26, 2018 18:49
PageIndex is 1-based? You monster!
July 27, 2018 15:29
Hi all!

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.
July 27, 2018 16:53
Awesome post!
July 30, 2018 16:36
Wonderful code test it's very useful for me. Thanks for posting.
Thank you
August 01, 2018 0:05
You say that's too small for a package yet left-pad exists

Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.