Monday, June 29, 2020

Misusing C#: Multiple Main() Methods

Sometimes you run across things you should probably never do in real life but are possible in your development environment. Having more than one "Main()" method in an application is one of those things in the .NET world.
An application can have more than one "static Main()" method. The only condition is that you must indicate which is the "real" entry point (through a project setting or on the command line).
Note: the code for this project is on GitHub: jeremybytes/misusing-c-sharp. Today's project is in the "MultipleMain" folder, but there will be other projects coming in the near future.

Let's look at a program that misuses this feature.

A Straight-Forward Console Application?
This is a console application with the following "Program" class (from the Program.cs file in the "MultipleMain" project mentioned above):

The "Program" class has a "static Main()" method that puts "Hello World!" on the console. If you supply a command-line parameter (such as your name), then it will say hello to you.

Since this is a .NET Core 3.1 project, lets go to the command line to build and run the application.

Build & Run
On the command line, a "dotnet build" will build the application.

Then we can use "dotnet run" to run the application.

Wait, what?

This isn't what I expected at all. What's happening?

static Main() Methods
This application misuses the normal entry point for .NET applications: "static Main()".
  1. "static Main()" is the entry point to an application.
  2. "static Main()" can be in any class.
  3. In C# 8, "static Main()" can be in an interface.
  4. You can only have one "static Main()" method per application.*
*Note: if you have more than one "static Main()" you must specify which one is the application entry point.

See that * above? It's actually very important. Normally, we are only allowed to have one "static Main()" in an application, BUT we can have more than one as long as we also tell the application which one is the entry point. This can be done with a compiler directive or with an application property. (We'll see this below.)

Another static Main() Method
Our project has another "static Main()" buried in the "DataTypes.cs" file in the "Library" folder.

I did a little bit of obfuscation with naming just for extra fun. In addition, if we open the "DataTypes.cs" file, we'll see some innocuous data types at the top of the file (from the DataTypes.cs file in the "Library" folder):

But if we scroll down a bit, we find another "static Main()" in the "IWhyNot" interface (from the same file):

This method generates 5000 random numbers before printing out "This is not what I expected!".

Note: I named this the "IWhyNot" interface in honor of my favorite comment in the language design notes. You can check out this article for more information: C# 8 Interfaces: Static Main() -- Why Not?

This is the current entry point for the application.

Specifying the "real" static Main() Method
The reason this application works this way (and builds at all) is because there is a "StartupObject" attribute in the project file.

Here is the project file (from the MultipleMain.csproj file on GitHub):

Notice the "<StartupObject>" tag. This has the fully-qualified name for the type that contains the "real" static Main() method. In this case, it is "MultipleMain.DataTypes.IWhyNot".

Overriding the StartupObject at Build
It is possible to override the "StartupObject" on the command line. Here's a new "dotnet build" that shows this.

dotnet build -p:StartupObject=MultipleMain.Program -t:Rebuild

The "dotnet build" command has a couple of parameters. The first is "-p:StartupObject=MultipleMain.Program".

The "-p" parameter adds a "property" to the build. Here we are specifying the value to use for the "StartupObject" property (this is the same property that we saw in the project file). The new value for this is "MultipleMain.Program" -- the "Program" class that we saw first.

The second parameter that we have is "-t:Rebuild"

The "-t" parameter sets a "target". This is important for our scenario.

Normally, when we "dotnet build", it uses the same methodology as "Build" from Visual Studio. If all the files in a project are unchanged, the project is not re-built. If we were to use a normal build here, then the executable would *not* be rebuilt because none of the source files (or even the project file) have changed.

So we need to specify that we want to do a "Rebuild". This is the same as "Rebuild" in Visual Studio. All of the projects are re-built whether the source files have changed or not.

So this "dotnet build" command forces a rebuild of our project using the "StartupObject" specified (and the command-line parameter overrides any values from the project file).

Now when we run the application, we get the expected results:

Using "dotnet run" gives us the expected "Hello World!". And if we use "dotnet run -- Jeremy" (which passes "Jeremy" to the program as a command-line parameter), we get the output "Hello, Jeremy".

Note: "dotnet run" will do a build if necessary. Like a normal build, this is based on changes to the source files. Since the source files are unchanged here, "dotnet run" uses the existing executable / assembly.

More Options
Because we have the command-line option for specifying the "StartupObject", we do not need to put it into the project file.

As an experiment, let's remove the "StartupObject" attribute from the "MultipleMain.csproj" file and try to build the application in Visual Studio.

We get the same error using "dotnet build" (with no additional parameters):

This gives us an error because we have not specified the entry point for the application. If we follow the error code (CS0017), we come across this article: -main (C# Compiler Options).

This shows us that we can use a "-main" command-line parameter when we're using csc.exe (the command-line C# compiler). The "-main" option is not exposed in MSBuild, and so is not available using the "dotnet build" command.

The other option in the article is to set the "StartupObject" property in the project file. And that's the route that we took here.

If we use "dotnet build" *with* the StartupObject property, then we can build successfully. We do not need the "StartupObject" attribute in the project file at all (as long as we supply the value at compile-time).

Be Kind
Please don't do this to your co-workers. Many developers are not aware of this feature. So if you bury a "static Main()" somewhere in the code and then set the "StarupObject" in the project file, another developer will probably spend a very long time figuring out what's going on.

Remember: Just because you can doesn't mean that you should.

Happy Coding!

Thursday, May 7, 2020

Cross-Platform Build Events in .NET Core using MSBuild

In the last article, we looked at post-build events and .NET Core -- specifically how the '$(SolutionDir)' and other macros based on the solution folder do not work when we do project-based builds. Instead, we should use folders relative to the project.

    xcopy "$(ProjectDir)AdditionalFiles\*.*" "$(TargetDir)" /Y

This works fine for Windows-based projects (like the WPF project used in the previous article), but it fails when we move to a cross-platform environment. "xcopy" doesn't exist on macOS or Linux.

Fortunately, we can dig into MSBuild a little deeper and create tasks that copy files in a platform-agnostic manner. Here's a preview of the build task that does the same things as above:

Let's dig into some code.

MSBuild Tasks
MSBuild (the build tool in the .NET world) has a number of Tasks that we can use as part of the build process. These go right into our project files.

In fact, when we add a post-build event through the Visual Studio project settings editor, we end up with an MSBuild task in the project file. For example, our "xcopy" command that we saw above:


Ends up in the project file looking like this:


  <Target Name="PostBuild" AfterTargets="PostBuildEvent">
    <Exec Command="xcopy &quot;$(SolutionDir)AdditionalFiles\*.*&quot; &quot;$(TargetDir)&quot; /Y" />
  </Target>

This uses the "Exec" task to run a command at the command prompt.

So we're already using MSBuild tasks. Fortunately for us, there are other tasks to choose from besides "Exec".

The Copy Task

On the Microsoft Docs site, we can see the tasks that are built in: MSBuild task reference. One of those is the Copy task (docs reference for Copy task).

For today's examples, I'll be showing the cross-platform examples from my C# Interfaces talk: https://github.com/jeremybytes/understanding-interfaces-core31. There are 2 specific project files that I'll link to as we go.

Let's look at our new copy task and see what's going on (this code is available in the "PeopleViewer.csproj" file at this link: PeopleViewer.csproj -- specifically lines 17-26):


First, let's look at the "Target". This is a grouping that tells when this task is going to run. Notice that "AfterTargets" parameter is set to "Build". So this will run after the build. If the build fails, then this will not run.

Inside, we have the "Copy" task itself. We are using 3 parameters: (1) SourceFiles, (2) DestinationFolder, and (3) SkipUnchangedFiles.

"SourceFiles" is a required parameter. This is an array that specifies the files that we want to copy. Since this is an array, we create our own element called "DataFiles". The "Include" parameter has a list of the files that we want to include. We can still use the build macros, so "$(ProjectDir)" works just fine here. In this case, we only have one item in our array (our one item happens to include a wildcard "*.*").

The value for the "SourceFiles" parameter is the item created in the ItemGroup above. We reference it with using "@(DataFiles)".

As an alternative to using wildcards, we can specify individual files separated by semi-colons. An example of this is shown on the Microsoft Docs site: Example (with multiple files) -- screenshot below (but be sure to follow the link):


"DestinationFolder" is a kind-of-required parameter. This has the destination for the files to be copied. If the folder does not exist, then it is created automatically. Our value for this folder is the "$(TargetDir)" that we had in the "xcopy" version.

"DestinationFiles" is a kind-of-required parameter. You'll notice that we do not have a "DestinationFiles" parameter for our copy task. This is because we can use either "DestinationFolder" or "DestinationFiles", but not both (and we need at to have at least one of them). The "DestinationFiles" parameter is also an array, and it expects a one-to-one mapping of items from the "SourceFiles" array.

"SkipUnchangedFiles" is an optional parameter. This will not copy files that are unchanged between the source and the destination. It determines this based on the file size and the timestamp.

There are a number of other interesting parameters, so you should take a look at the docs. For example, "UseHardlinksIfPossible" will create links to the files rather than copying them.

Recursive Copying
Many times we need to copy entire folder structures (including subfolders). The good news is that we can do that with the copy task as well. For more information, take a look at the example on the Microsoft Docs site: Example (recursive copy) -- screenshot below (but be sure to follow the link):


A Lot of Effort?
This seems like a lot of effort compared to our previous post-build event. With the post-build event, we can use the Visual Studio project editor to build our commands and insert macros. Here, we need to edit our project files by hand. But it is totally worth it. This command works on Windows, macOS, and Linux -- and yes, I have tried it in all of those environments.

Before I found the MSBuild tasks, I was thinking about how I would get around this. Could I put in different commands (like using "xcopy" for Windows and "cp" for macOS), and then add some sort of conditional? That is technically possible, but it is a lot more brittle when all I need to do is copy some files.

I definitely prefer to use the build task.

OS-Specific Tasks
I ran into an issue where I needed to copy different files based on the OS, so I ended up digging into conditional MSBuild tasks.

For my specific issue, I have a library that uses SQLite to access a local database file. I need to do runtime binding to this library, so I need all of the required files in a shared location. The problem is that SQLite has different runtime files for each environment. For Windows, it uses "e_sqlite3.dll", for macOS it uses "libe_sqlite3.dylib", and for Linux it uses "libe_sqlite3.so". I needed to figure out how to get the right file to the output folder.

The MSBuild tasks for this are in the "PersonReader.SQL.csproj" file which is on GitHub: PersonReader.SQL.csproj.

Windows-Specific Task
To get started with conditional tasks, let's look at the task that copies over the Windows-specific SQLite file (this is on lines 31-41 of the PersonReader.SQL.csproj file):


Overall, this task looks similar to what we saw above. One difference is that the Target has a "Condition" parameter.

Condition=" '$(OS)' == 'Windows_NT' "
"Condition" lets us put in a restriction on when this task will run. For this example, we can look at the '$(OS)' value to see if we are running under Windows. If so, then it runs the copy task.

For more details on the types of conditions that we can use here, check out the MSBuild conditions article on Microsoft Docs.

macOS-Specific Task
It seems like doing the same thing for macOS should be straight-forward: just change the condition to use "macOS". Unfortunately, that does not work here. The '$(OS)' values are not specific enough for what we need. If we run on macOS, we get the value of "Unix" (since macOS is a Unix-based system). The problem is that for Linux, the value is also "Unix". So the "$(OS)" value is not helpful.

Fortunately, there is another option. Here is the code (from lines 43-53 of the same PersonReader.SQL.csproj file):


This condition is a bit harder to understand because things are fully-qualified. So before we look at this condition, let's look at this another way: as C# code.

Checking the OS in C#
If we wanted to check for the OS in C#, we can use the "RuntimeTypeInformation" class that is part of System.Runtime.InteropServices. Here's the code:


    if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {}

The "IsOSPlatform" method takes an enum value (OSPlatform) as a parameter. So we can use this method to check whether something is running under Windows, macOS (i.e. OSX), or Linux.

For those of you who are new to macOS, the operating system was renamed from "OSX" to "macOS" a few years back. Since it is generally a bad idea to change enums that are already in use, we'll see "OSX" in various places when referencing the Apple OS.

Back to the MSBuild Condition
The condition that we have in the MSBuild task above uses this same "IsOSPlatform" method, but the syntax is a little different because it is in the MSBuild format and also because the names are fully-qualified.

Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)))'"
If we pull out the fully-qualified parts, we get something a bit more readable:

Condition="'$([RuntimeInformation]::IsOSPlatform($([OSPlatform]::OSX)))'"
The syntax is still different, but it's easier to see that we are calling the "IsOSPlatform" method on the "RuntimeInformation" class, and that we are passing in the equivalent of "OSPlatform.OSX" as a parameter.

Now this will not work unless it is fully-qualified, so we need to use the long version in our MSBuild task.

Linux-Specific Build Task
The task for Linux is similar to the macOS task. If you're interested, you can look at the code in the PersonReader.SQL.csproj file (lines 55-65).

Wrap Up

Now we have a post-build copy command that works cross-platform. And we also have some conditional commands that run based on what operating system we are building on. This gives us quite a bit of flexibility in building truly cross-platform applications.

MSBuild is quite an extensive system, and there are tons of options. The MSBuild task reference has a list of built-in tasks. In addition, you can create custom build tasks if needed (although that is something I am not likely to do myself).

One of the most interesting things to me about .NET Core is the cross-platform capabilities. As I move my applications forward, I think about how I can make them easier to run cross-platform. This gives a bigger audience for the applications and also provides different options for deployment.

Copying files in a way that is friendly to multiple operating systems is just one of the things we need to think about when creating cross-platform applications. Be sure to check back; I'll continue to document things I come across as I move projects over.

Happy Coding!

Tuesday, May 5, 2020

Post-Build Events and .NET Core

I have used post-build events in Visual Studio for a long time, primarily to copy files from one location to another. But due to changes in how we work with .NET Core, I have changed the way I write build events.

Short version:
The '$(SolutionDir)' macro doesn't work well from the command line. Replace references to the Solution with references relative to the current Project.
If you'd like an overview on using build events (and the macros that are available), take a look at this article: Using Build Events in Visual Studio to Make Life Easier.

Using a Post-Build Event
Let's look at an example of how I use post-build events. Here's the folder structure for a solution:


This solution has 4 projects: (1) "Common" is a shared library with cross-cutting classes defined; (2) "PeopleViewer" is a desktop application, (3) "PeopleViewer.Presentation" is a view-model class library, and (4) "PersonReader.CSV" is a data reader.

The top folder, "AdditionalFiles", contains a text file with the data. The "PeopleViewer" needs access to this file in order for the application to work. So, in this scenario, I want to copy the contents of the "AdditionalFiles" folder into the output folder for the "PeopleViewer" project.

Here is a post-build event that does this:


    xcopy "$(SolutionDir)AdditionalFiles\*.*" "$(TargetDir)" /Y

The "$(SolutionDir)" macro expands into the fully qualified path for the solution. This includes the trailing "\". The "$(TargetDir)" macro expands to the fully-qualified location where the .exe file ends up -- something like "[fullpath]\PeopleViewer\bin\Debug\" for .NET Framework and "[fullpath]\PeopleViewer\bin\Debug\netcoreapp3.1" for .NET Core.

This works fine when we build the application from Visual Studio:


    4>C:\Development\Sessions\DI\Scratch\AdditionalFiles\People.txt
    4>1 File(s) copied

The build succeeds and the file is copied.

The problem arises when we leave Visual Studio.

.NET Core and the Lack of Solutions
In the .NET Core world, the concept of the "Solution" is less important. We often open folders (not solutions) in Visual Studio Code. And when we use the command-line tools, we build projects (not solutions). The ".sln" file is not important in this scenario, and it is generally not used at all.

This works because the projects have all of the information that they need to build, including dependencies on other projects and on NuGet packages.

So to build the "PeopleViewer" project, I can open the command line to the project location and type "dotnet build".

But, the post-build event fails:


  0 File(s) copied
  File not found - *.*
C:\Development\Sessions\DI\Scratch\PeopleViewer\PeopleViewer.csproj(15,5): error MSB3073: The command "xcopy "*Undefined*AdditionalFiles\*.*" "C:\Development\Sessions\DI\Scratch\PeopleViewer\bin\Debug\netcoreapp3.0\" /Y" exited with code 4.

The message tells us that the copy step failed. And if we look closer at the message, we can see why:


    "xcopy "*Undefined*AdditionalFiles\*.*"

When trying to expand the macros, the '$(SolutionDir)' is undefined. There is no concept of a solution at this level, just the project. So the build fails.

Relative to the Project
The fix for this is to not use any of the "Solution" macros in the post-build events. Instead, set things relative to the current project.

Here's an updated post-build event:


    xcopy "$(ProjectDir)..\AdditionalFiles\*.*" "$(TargetDir)" /Y

To get to the "AdditionalFiles" folder, we start with the current project folder, then go up a level (with the "..").

When we build this from Visual Studio, it still works. But more importantly, it works when we build from the command line:


  C:\Development\Sessions\DI\Scratch\PeopleViewer\..\AdditionalFiles\People.txt
  1 File(s) copied

Success!

Summary
Post-build events are really useful, and I'm often using them to copy data files or late-bound assemblies to the output folder of my projects. But I've changed how I use them.
Instead of using post-build events relative to the Solution, I use events relative to the Project.
It's always exciting to come up with a fix to an issue. But this isn't completely solved.

Cross-Platform Concerns
One of the really awesome things about .NET Core is that it is cross platform. I can create applications that will run on Windows, macOS, and Linux.

But "xcopy" is a Windows command. How do we copy files for other OSes?

For that we need to dig into MSBuild. That will wait for the next article, but here's a preview:


This block in the project file will copy the files from the "AdditionalFiles" folder to the output folder. And it works on macOS and Linux.

We'll explore that in the next article.

Update: That article is now available: Cross-Platform Build Events in .NET Core using MSBuild.

Happy Coding!

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). You can also have .vbhtml files. (I'm not sure if .fshtml files are available yet, but they should be).

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!