← All field notes TALKS · No. 002

The Console App Framework Nobody Asked For (But Every .NET Developer Needs)

Let's talk about console applications. And let's bring mvc into it.

The Console App Framework Nobody Asked For (But Every .NET Developer Needs)

Say Whaaaaat ??!

Not the glamorous kind. Not the polished CLI tools with beautiful help text and a Homebrew formula. I mean the real ones — the internal tools, the migration scripts, the admin utilities, the ones that began as a 200-line Program.cs and somehow turned into something nobody wants to touch because the while(true) loop in the middle is doing structural work.

You know the one.


The Architecture Nobody Bothered With

What bothers me about console app development in .NET is that we already have proper architecture for web apps. ASP.NET Core MVC gives you controllers, routing, model binding, middleware, dependency injection, a view engine, and form posting. It is a complete pattern that scales from a weekend project to a production system.

Console developers get Console.WriteLine().

There is a strange assumption in the ecosystem that console apps are just throwaway scripts. They do not need architecture. They are not real applications. You are not supposed to care.

I care. ConsoleMVC is what happened when I cared too much.


What It Actually Is

ConsoleMVC brings the Controller-ViewModel pattern to .NET console applications. If you have used ASP.NET Core MVC before, the mental model will feel familiar.

public class HomeController : Controller
{
    public ActionResult Index()
    {
        var model = new HomeViewModel
        {
            Title = "Welcome",
            Message = "Select an option below."
        };

        return View(model);
    }
}

Controllers inherit from Controller. Actions return ActionResult. Views are discovered by convention. Models are plain DTOs. RedirectToAction() works the way you expect it to.

At startup, the framework scans the assembly, discovers controllers and views through reflection, and starts the main loop at the default route. There is no manual registration and no XML config file. There is also no "Settigns" typo quietly sending the app nowhere.


The .cvw Files

This is where it gets more interesting. Views use a .cvw file format, which stands for Console View. It is plain C# with an @model directive at the top, compiled into ConsoleView<TModel> classes at build time by a Roslyn source generator.

@model MyApp.Models.DashboardViewModel

Console.WriteLine($"=== {Model.Title} ===");
Console.WriteLine(Model.Message);

Console.Write("Select: ");
var input = Console.ReadLine()?.Trim();

return input switch
{
    "1" => NavigationResult.To("Home", "About"),
    "q" => NavigationResult.Quit(),
    _   => NavigationResult.To("Home", "Index")
};

There is no class boilerplate and no interface implementation to write by hand. You write the view logic and the generator handles the plumbing. Every view has to return a NavigationResult, which tells the framework where to go next or when to quit. The type system enforces that for you.


Form Posting

This is the feature that surprised people the most, and probably the one that turned out to be the most correct architecturally.

Views can collect user input, package it into a Dictionary<string, string>, and post it to a controller action, just like an HTML form post. The framework binds those values to the target action's parameters automatically and handles type conversion along the way.

// In the view
var formData = new Dictionary<string, string>
{
    ["Name"] = name,
    ["Color"] = color
};

return NavigationResult.ToAction("Result", formData);

// In the controller
public ActionResult Result(GreetFormModel model)
{
    if (string.IsNullOrWhiteSpace(model.Name))
        return RedirectToAction("Index");

    return View(new GreetResultModel { Greeting = $"Hello, {model.Name}!" });
}

Keeping input collection in the view and business logic in the controller is the whole point of MVC. Mixing them is how you end up back in the six-hundred-line Program.cs. The form posting pattern keeps that separation in place even when it is inconvenient, which is usually when it matters most.

The model binder supports complex types, simple parameters, nullable variants, enums, GUIDs, and case-insensitive key matching. Missing keys and conversion failures fall back gracefully.


First-Class IDE Support

Working in a custom file format without IDE support is miserable, so there is a JetBrains Rider plugin that treats .cvw files as first-class citizens.

What you get:

  • Syntax highlighting for @model, @using, and the C# body.
  • Code completion for directives, NavigationResult methods, Console.*, Model, and ViewData.
  • Error highlighting for missing @model, empty arguments, missing return statements, and model type mismatches.
  • Navigation through gutter icons, NavigationResult.To() targets, and Go to Related.
  • Refactoring support so renaming or moving a model class propagates into .cvw files through the ReSharper backend.
  • Live templates like cvw, navto, navaction, and navquit.
  • Quick fixes for missing @model and missing return statements.

The plugin plugs into the ReSharper backend for proper semantic C# analysis. It is not pretending to understand your code. It actually does.


Getting Started

Install the project template:

dotnet new install ConsoleMVC.Template

Scaffold a new app:

dotnet new consolemvc -n MyApp
cd MyApp
dotnet run

That gives you a ready-to-run application with controllers, models, views, and a working form posting example out of the box. Add the Rider plugin, associate *.cvw with C# in your editor of choice, and you have a full development experience.

The framework package is ConsoleMVC.Framework on NuGet. The template is ConsoleMVC.Template.


What Is Next

The current framework is a working architectural foundation. The part I am more excited about is C-View Markup, a markup language built specifically for terminal UIs.

Imagine writing a console screen like this:

<box title="Dashboard" width="60" border="double">
    <text align="center" color="cyan">Welcome, @Model.UserName!</text>
    <table>
        <row><cell>Notifications</cell><cell>@Model.Count</cell></row>
    </table>
</box>

<menu prompt="Select an option:">
    <option key="1" nav="Home/Index">Home</option>
    <option key="q" quit="true">Exit</option>
</menu>

That would not be HTML, and it would not be a wrapper around some existing library. It would be a domain-specific markup for terminals, compiled at build time by the source generator into rendering code. Layout primitives, colour, alignment, and declarative input collection would all be part of the same flow instead of something you hand-roll with Console.WriteLine().


Console Apps Deserve Better

ConsoleMVC is an open-source project built around a simple idea: console applications are real applications. They deserve the same architectural patterns, tooling, and developer experience as web applications.

The framework is still young, and the rough edges are real. But the foundation is solid, and the direction is clear.

If you have built frameworks, rendering engines, or markup parsers before, or if you just think the problem is worth solving, the repository is open, issues are read, and PRs are welcome.


Read more about the project and see it in context at podeszwa.dev/portfolio/consolemvc.