Tuesday, February 18, 2020

The Secret Code: ASP.NET MVC Conventions

ASP.NET MVC is filled with hidden conventions: put a controller or view in a certain place and give it a special name, and it magically works. If you know the conventions, you can quickly get an application up and running. But if you don't know the secret code, all is lost.

Today, we'll look at some of the conventions used in the ASP.NET Core MVC standard template:
  • File Naming and Locations
  • Basic Routing
  • Views and Layout
This will be just enough to get you started. Once you have the basic idea, you can find lots of articles that dig into each topic. Stephen Haunts (@stephenhaunts / https://stephenhaunts.com/) and Shawn Wildermuth (@ShawnWildermuth / http://wildermuth.com/) are both good resources for deeper dives.

The code for this project is on GitHub: jeremybytes/mvc-conventions-aspnet. However, the code is from the standard ASP.NET Core MVC project template, so you can create a new project locally and see the same thing (at least with the "current" .NET Core 3.1 templates). I will use Visual Studio Code to explore the files and run the project, but you can use the editor/IDE of your choice.

New ASP.NET Core MVC Project
We'll start by creating a new ASP.NET Core MVC project. I'll use the command-line for this in PowerShell (but these commands are the same from cmd.exe or bash).

From the command line, type dotnet new mvc -n "mvc-conventions'


"dotnet new" indicates we want to create a new project, "mvc" is the template for an ASP.NET Core MVC project. The "-n "mvc-conventions"" says we want to use the name "mvc-conventions" for the project.

This creates a folder called "mvc-conventions" with our new project.


Running the Application
The template has enough code for us to run. From the command line, type "dotnet run" to start the web application. Note: be sure that you are in the "mvc-conventions" folder.


Notice that the application says it is listening at "https://localhost:5001" and "http://localhost:5000". By default, the website forwards http calls to https, so if we navigate to either of these locations, we'll end up at the https location.

Note: If you do not have a localhost developer certificate set up, you may get browser warnings when going to the https site. Take a look at this article from Scott Hanselman for how to fix this: Developing locally with ASP.NET Core under HTTPS, SSL, and Self-Signed Certs.

Here's the site in the browser:


The template site only has a couple of features. The above is the "Home" screen. We can also navigate to the "Privacy" screen by clicking one of the "Privacy" links.


The Project Files
Let's take a closer look at the generated code. I have Visual Studio Code installed. To open Visual Studio Code at the current folder, we can use "code ." on the command line.


When the project opens, you will get a popup asking to add required assets. Go ahead and click "Yes" to this. This creates a ".vscode" folder that has some settings to make things easier to run and debug from within Visual Studio Code.

Here is the structure that was created (also available from the GitHub project):


The items we'll concentrate on today are the "Controllers" folder, the "Views" folder, and the "Startup.cs" file.

A Quick Overview of MVC
Before going further, we should get a quick overview of what MVC is. MVC stands for Model-View-Controller, and it is a common application UI pattern.

The Model is the data/logic of the application. This application doesn't really have that since we're dealing with just a couple of hard-coded pages.

The Controller is responsible for interacting with the Model and then displaying a View.

The View is the UI. This is what the user sees in the browser.

This is a highly-simplified description of the elements. In the case of ASP.NET MVC, we will call an action on a controller. That action will perform some type of work (often interacting with the model) and determine what view to send back to the user.

We'll walk through this by looking at a route.

The Default Route
Routing is how we turn URLs into actions. This is a very large topic. For today, we'll just look at the default route that we get with the template.

The default route is set up in the Startup.cs file at the root level of our project (from the Startup.cs file on GitHub):


We'll break this down a little bit.

First, the pattern "{controller}/{action}/{id}" says that we're trying to parse a controller, an action, and an id from the URL.

Convention: The default route pattern is [Controller Name] / [Action Name] / [ID (optional parameter)].

So if we have a sample URL of "https://localhost:5001/Home/Index/2", we would use the "Home" controller by calling the "Index" action on it, and pass in an "id" parameter of "2".

The pattern in the code is a little more complex than what I just showed. That is because there are some default values set up. If a "controller" is not provided, then by default we use the "Home" controller. In addition, if an "action" is not provided, by default we use the "Index" action.

The "?" on the "id" specifies that this is optional. So if it is not provided, then there's no harm done.

With the defaults, that means that if we navigate to "https://localhost:5001", then it will try to call use the "Index" action on the "Home" controller (since those are the defaults).

The Controller and Action
Now that we know what controller and action to use, how does the system find it? That's where some of the conventions come in.

Convention: Controllers are located in the "Controllers" folder.

Controllers are located in the "Controllers" folder. Here's that folder in our project (you can also look at the Controllers folder on GitHub):


This has one file: HomeController.cs. Here is the code for the "HomeController" class with some of the methods collapsed (from the HomeController.cs file):


Convention: Controllers are named with the suffix "Controller".

Due to the naming conventions, if our routing is looking for the "Home" controller, it will look for a class named "HomeController". The naming of our controllers is important if we want ASP.NET Core MVC to be able to find them automatically.

So we've found the controller, what about the action "Index"?

When looking for an action, we're really looking for a function of the same name on the controller.

Convention: An Action is a function with the same name in the Controller class.

For the "Index" action, we would look for an "Index" function on the controller. In addition, the method should return an "IActionResult". We won't get into what all the valid values are for "IActionResult". Today we'll just concentrate on Views.

Here is the "Index" method on the controller (from the HomeController.cs file):


Generally, an action will perform some type of work, such as get data from the model or process form data. In this case, we only return a View without passing anything programmatically.

The next question is "How do we find the View?"

The View
As you might imagine, there are conventions around how views are named and located (just as with controllers).

Here are the views in the project (from the Views folder on GitHub -- you'll have to drill down to see all the files):



Convention: Views are in the "Views" folder with sub-folders based on the Controller name.

Under the "Views" folder in our project, we have a "Home" folder. These are the views that are associated with the HomeController. In that folder, we have 2 files, "Index.cshtml" and "Privacy.cshtml".

Convention: Views are named after the corresponding Action in the Controller.

The Views correspond to 2 of the actions on the HomeController: Index and Privacy.

So when the "Index" action returns "View()", it uses the "Index.cshtml" file in the "Home" folder under "Views".

Technical note: these conventions are based on how the ASP.NET MVC infrastructure searches for views. First, it looks in the "Views" subfolder that matches the Controller name. If it cannot find a matching view there, then it looks in the "Shared" subfolder. As you dive deeper into ASP.NET MVC, you can read articles that go into this in more detail.

Here's the code for the "Index" view (from the Index.cshtml file):


This has the code that is unique to this page, but it is incomplete. It's just a "div", not a complete web page. (We'll get back to what the "@" section means in just a bit.)

The rest of the page is part of a shared layout.

The Layout
If we look at the "Views" folder again, we see that there is also a "Shared" sub-folder.


This contains views that are not particular to any controller (such as the "Error" view). But it also has the "_Layout.cshtml" file. This is what fills in the rest of the page that we see in the browser.

Here is part of the content from the layout (from the _Layout.cshtml file):


This file has the markup for the headers and footers of the page (including the menus). It also brings in style sheets and Bootstrap.

Injecting the Body of the View
The important bit for us is the code "@RenderBody()".

In .cshtml files, the "@" means "there's some C# code coming!" 

The extension of the file (cshtml) indicates that it is a mixture of C# (cs) and HTML (html).

So when we have "@RenderBody()", this is an indication that the "RenderBody" method should be run. "RenderBody" is an included helper method, and this is what puts the custom view markup into the page.

So in our case, the "div" that is in the Index.cshtml file ends up inside the "main" tag from the layout file.

Other Injected Values
The layout is not limited to "@RenderBody()". The custom views can also provide data to the layout through ViewData.

Let's go back to the Index View for a moment (from the Index.cshtml file):


Notice the "@" section at the top of the page. As we saw earlier, this indicates a section of C# code. In this case, it is blocked by curly braces; everything inside the braces is considered the C# code. "ViewData" is a dictionary that lets us set up name/value pairs. In this case, we have the "Title" for the page.

In the header section of the layout page, we can see how this is used (from the _Layout.cshtml file):


Notice that inside the "title" tag of the page head, there is "@ViewData["Title"]". Again, the "@" indicates that this is C# code. This takes the value from the Index.cshtml ViewData and puts it in here.

So when we view the page, we can see the page title set in the browser tab:


The value is "Home Page - mvc_conventions".

Note: Our pages do not need to use _Layout.cshtml. We can have some pages use the layout and some pages that don't. Or we can eliminate the layout completely. If you're interested, you can find articles that talk about this in more detail.

Routing to Other Actions
If we click on one of the "Privacy" links, we can see the route to that page:


The URL is "https://localhost:5001/Home/Privacy". Based on what we've seen, we should be able to figure out where this goes.

If we go back to the default route: Controller / Action / Optional ID, we can translate this URL to using the "Privacy" action on the "Home" controller.

The "Privacy" action translates into the "Privacy()" function on the controller (from the HomeController.cs file):


This returns a view just like the Index action.

But instead of using the "Index.cshtml" file, it uses the "Privacy.cshtml" file. Here is the code for that (from the Privacy.cshtml file):


This has ViewData specified with a title as well. The layout puts this into the "title" tag of the page. But the Privacy View also uses it in an "h1" tag on the page itself.

Using ViewData is one way to get data from the Controller to a View, but that's outside the scope of what we're looking at today.

Adding Your Own Pages
With an understanding of the conventions that are part of ASP.NET MVC, we can add our own pages fairly easily. If we wanted to add a page with a catalog of products, we would create a class called "CatalogController" and put it in the "Controllers" folder.

Then in the "CatalogController" class, we could add an "Index" method that calls into the Model to get a list of products from a database, and return that data along with the View.

To create the view, we can add an "Index.cshtml" file to a newly created "Catalog" folder under "Views".

If we follow the naming conventions, then we do not have to do anything special to wire the page up. The conventions take care of all of that for us. We would just access it with "https://locahost:5001/Catalog" (remembering that "Index" is the default action).

We could even take this a step further and create a "Product" action that takes an "id" parameter. This could get a product from the Model based on the id and then display it in a custom View. This could be accessed with "https://localhost:5001/Catalog/Product/24". And if the default routes don't work for us, we can create custom routes as well.

These are the conventions I used to make the web front end for my maze generation project. You can read more about that and dive into some basics about parameters here: Building a Web Front-End to Show Mazes in ASP.NET Core MVC.

Wrap Up
ASP.NET Core MVC (as well as ASP.NET MVC on .NET Framework) have a lot of conventions. This saves us from writing a lot of configuration code to wire things together. But we do need to be aware of the conventions in order for our pages to work properly.

Summary of Conventions:
  • The default route pattern is [Controller Name] / [Action Name] / [ID (optional parameter)].
  • Controllers are located in the "Controllers" folder.
  • Controllers are named with the suffix "Controller".
  • An Action is a function with the same name in the Controller class.
  • Views are in the "Views" folder with sub-folders based on the Controller name.
  • Views are named after the corresponding Action in the Controller.
Other Stuff:
  • In .cshtml files, the "@" means "there's some C# code coming!"
With these conventions in hand, it is easier to understand what is happening in existing ASP.NET MVC web projects that you need to modify. And it is also a good starting point to take a deeper dive into all things MVC.

Happy Coding!

Wednesday, February 5, 2020

Set the Working Directory in Visual Studio Code (or better yet, eliminate the requirement on the working directory)

In the last article, I showed what appeared to be a bug in the Visual Studio Code debugger and the .NET Core CLI (command-line interface). The issue stems from the fact that Visual Studio Code and Visual Studio 2019 use different  default values for "working directory" when debugging.

Default "Working Directory":
  • The Visual Studio 2019 debugger uses the executable directory
  • The Visual Studio Code debugger uses the project directory
The result is that an application that relies on the "current working directory" to find files will fail in strange ways. And that's just what the last article showed.
In Visual Studio Code, you can change the debugger / runner working directory in the "launch.json" file.
The code for this article is available on GitHub: jeremybytes-understanding-interfaces-core30. We are specifically using the completed/04-DynamicLoading-Plugin folder. To make things more interesting, some of the code samples use the "master" branch, and some use the "UsingWorkingDirectory" branch. These branches will be noted when code is shown.

Setting the Working Directory in Visual Studio Code
The specific application we are running is "PeopleViewer" -- a WPF application that uses a dynamically-loaded SQL data reader.

Here is the default "launch.json" file that was created by Visual Studio Code for the PeopleViewer project (from the launch.json file on the "UsingWorkingDirectory" branch):


This has a "cwd" setting which stands for "current working directory".

The default for this is the "${workspaceFolder}" which in this case means the project folder for the PeopleViewer application.

We can also see that the "program" setting references the output folder (workspace + bin/Debug/netcoreapp3.1/). This is the program that will be executed when running or debugging from Visual Studio Code.

Side note: .NET Core creates dlls for everything (including the desktop application that we have here). This dll can be executed from this directory on the command line using "dotnet .\PeopleViewer.dll". In addition, the compiler creates an executable ("PeopleViewer.exe") to make it easy to run as a separate application.

We can fix the issue by setting the working directory to be the same as the output folder:


Now the value is "${workspaceFolder}/bin/Debug/netcoreapp3.1". This will set the working directory to the same as our executable.

And if we run the application from Visual Studio Code (with or without debugging), it will work as expected.


SUCCESS!

Sort of. This doesn't eliminate all of the issues that we saw.

Issues from the .NET Core CLI
This doesn't change the problem that we saw in the last article when running the application from the .NET Core CLI.

We can open the command line to the project folder and type "dotnet run":


The application starts


But if we click the button, the application exits (with an unhandled exception).


This shows that we have the same problem with the working directory. The CLI tools do not use the "launch.json" file.

A better solution is to eliminate the reliance on the working directory.

Eliminating the Requirement on the Working Directory
This application uses a SQLite database which is in the "People.db" file on the file system. The application build process makes sure that the "People.db" file is copied into the output folder, so that "PeopleViewer.exe" and "People.db" are both in the same folder.

Here is the configuration for the SQLite EFCore DBContext (from the SQLReader.cs file on the "UsingWorkingDirectory" branch):


This uses the setting "Data Source=People.db". This tells the SQLite driver that we are using a file called "Person.db". The problem is that it looks for that file in the working directory.

Because it looks for the file in working directory, the application runs fine when executed from the application folder (either from File Explorer or from the command line). It also works in the Visual Studio 2019 debugger that uses the application folder as the working directory. And it will work from Visual Studio Code if we set the "cwd" value as shown above.

The Problem
The way that I discovered this was a problem was with Git. When I went to look at changes to the project, I noticed that there was a new "People.db" file in the project folder.


This is what led me to the discovery. The "People.db" file should not be in this folder; the source is somewhere else (an "AdditionalFiles" folder), and it is explicitly copied to the output folder.

So how did it get here?
If SQLite does not find the database file, it creates a new one.
Since this new file is empty, it explains the exception which says it cannot find the "People" table in the database.

Fixing the Issue
What we really want to do is fix the database connection so that it does not rely on the working directory.

A CSV data reader in this same project does not suffer from this problem even though it is also looking for a file (People.txt) in the same folder as the executable. This is because the CSV data reader is more explicit about the file location. Here is the configuration code (from the CSVReader.cs file on the master branch):


This uses the path "AppDomain.CurrentDomain.BaseDirectory" as the path to find the text file. This value is the same folder as the executable, so it works regardless of the current working directory.

To fix the SQL data reader, we can add the same path (from the SQLReader.cs file on the master branch):


Now we have "Data Source={AppDomain.CurrentDomain.BaseDirectory}People.db".

This adds the full directory to the "People.db" data source value, so SQLite will be able to find this file more reliably.

Now if we re-build everything (making sure to explicitly rebuild the "PersonReader.SQL" project so that the output .dll gets to the right folder), the application now runs fine from the .NET Core CLI.


"dotnet run" starts the application.


And if we click the "Dynamic Reader" button...


It works!

Awesomeness and Frustration
One thing that I really like about .NET Core is the set of options we have to build and run. We can use Visual Studio 2019; we can use Visual Studio 2019 for Mac, we can use Visual Studio Code, or we can use the .NET Core CLI.

My goal when building samples in .NET Core is for them to run as consistently as possible in these various development environments. This particular application will not work with Visual Studio 2019 for Mac (since it relies on WPF -- a Windows-only solution), but my website that generates mazes does work on macOS and Linux.

Finding the differences in the working directory among Visual Studio 2019, Visual Studio Code, and the .NET Core CLI was frustrating. I was sidetracked into figuring out why this was working in some places but not others. I "accidentally" ran across a clue by finding the unexpected "Person.db" file. Without that, I was completely lost on the cause. And that's quite a frustration.

But finding this problem also led to a more robust solution. Now that I know that SQLite relies on the current working directory, I can make sure that my connection strings are more specific. This is better overall.

I'll be sure to add more hurdles and solutions as I come across them. Hopefully this will be a help to someone else.

Happy Coding!