Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
162328 stories
·
33 followers

Rails to ASP.NET Core: A Developer's Map

1 Share

While it feels like many software developers have picked their bread and butter frameworks these days, there are still opportunities to explore other technologies and ecosystems. What makes adoption difficult is fighting our muscle memory and looking for familiar signposts. Our experience can be both our biggest advantage and disadvantage when moving between stacks.

This guide maps Rails conventions, Action Pack, Hotwire, Active Record, Sidekiq, and the Rails CLI to their closest ASP.NET Core and .NET counterparts. It began as an answer to a Reddit user’s question and is a conceptual map rather than a migration guide.

It is for full-stack Rails developers and API-focused Rails developers who want to expand their technical toolbox, evaluate .NET for a future project, or simply see how the other side lives.

How Do Rails and ASP.NET Core Handle Convention Over Configuration?

Convention over configuration reduces ceremony by making predictable choices on a team’s behalf. Rails is strongly opinionated: its default directory layout, naming rules, ORM conventions, generators, and integrated components guide most applications toward a familiar shape. That consistency makes it easy to move between Rails projects and quickly recognize where code belongs.

ASP.NET Core also has opinions, but they are more loosely held. Think of it as “opinions lite.” Its templates provide conventional layouts, dependency injection, configuration, logging, routing, and framework-specific patterns such as MVC controllers or Razor Pages. Unlike Rails, however, ASP.NET Core expects you to compose the pieces you need and readily permits multiple valid approaches to the same problem.

For a Rails developer, this is the important adjustment: ASP.NET Core MVC will feel familiar because it has controllers, views, routing, conventions, and validation. But the framework will ask you to make more decisions about project structure, data access, background processing, and UI architecture. That flexibility is useful when a project needs it, but it also means teams should agree on their conventions early.

How Does Ruby’s Dynamic Typing Compare to C#?

The biggest difference Ruby developers will notice in .NET is C#’s static type system. Neither approach is inherently better, but they move feedback to different points in the development cycle. Ruby gives you flexibility at runtime and leans heavily on tests to verify an object’s shape and behavior. C# asks you to declare those shapes up front and lets the compiler catch many mismatches before the application runs.

In daily work, that means renaming a property or changing a method signature in C# usually produces a list of compiler errors that points to every affected caller. You will also encounter nullable reference types, which let the compiler warn when code might use a missing value. Tests are still essential in .NET, but the compiler becomes an additional quality gate alongside them.

Rails developers will most often model application concepts with classes or records, then use interfaces when they need to describe a contract independent of a particular implementation. At the edges of an application, it is common to use explicit request and response models instead of passing database entities directly between controllers, views, APIs, and persistence. This can initially feel more verbose than Ruby, but it makes those boundaries and their expected data clearer to both the compiler and the next developer reading the code.

What Is the ASP.NET Core Equivalent of Action Pack?

Rails’ core web framework system is called Action Pack. It provides the familiar request pipeline: routing accepts an incoming request, selects a controller action, binds input, and renders a response.

ASP.NET Core has multiple web frameworks built on its endpoint routing system. MVC, Razor Pages, Minimal APIs, and Blazor all use the same hosting, configuration, dependency injection, middleware, logging, and routing foundations. The programming model you select determines how you organize endpoints and render a response.

For a Rails developer looking for the closest mental model, start with ASP.NET Core MVC. The concepts will feel familiar:

Rails ASP.NET Core MVC
config/routes.rb Endpoint route configuration in Program.cs and route attributes
Controller action Controller action method
Action Controller filters Middleware, endpoint filters, and MVC filters
Strong parameters Input models, model binding, and validation
Action View template Razor view

The models are analogous, not identical. Rails supplies stronger defaults around its application structure, while ASP.NET Core gives teams more latitude to decide how routes, services, data access, and features are organized.

Other ASP.NET Core programming models are appropriate in specific situations:

  • Razor Pages is a good fit for page-centric, server-rendered applications, especially CRUD screens. A Razor Page keeps a page’s markup and its request-handling code together, but it does not replace your domain or persistence models.
  • Minimal APIs work well for small HTTP services, focused endpoint groups, and teams that prefer endpoint-first code without controllers.
  • Blazor is a component framework for interactive .NET user interfaces. It is a different model from Rails’ controller-and-template workflow and deserves consideration when your team wants to build UI components in C#.

Start with the ASP.NET Core fundamentals, then build the official MVC tutorial or review the Razor Pages introduction. When you want to study working applications instead of another tutorial, Awesome .NET provides a broad index of .NET projects. For focused ASP.NET Core examples, Practical ASP.NET Core has runnable samples covering MVC, Razor Pages, Minimal APIs, htmx, authentication, and hosted services.

What Is the .NET Equivalent of Action View and Hotwire?

The UI layer is where Rails developers are most likely to encounter overwhelming choice. You can continue building server-rendered HTML, use a small amount of JavaScript to update parts of a page, or adopt a fully interactive component model. ASP.NET Core supports each approach, but it does not prescribe a single default in the way Rails does. Again, “opinions lite” as opposed to Rails strong opinions.

Razor is ASP.NET Core’s server-side HTML templating syntax and is the closest counterpart to Action View. Razor views and Razor Pages render HTML on the server. For reusable pieces of UI, use partial views, View Components, or tag helpers, depending on whether the UI needs only markup, needs to run server-side code, or benefits from HTML-oriented conventions.

If your Rails team likes Hotwire and Turbo, htmx is worth evaluating. Both approaches keep the server responsible for rendering HTML and let the browser replace targeted portions of a page instead of requiring a JSON API and a client-side framework. They are similar, not interchangeable: Turbo provides its own conventions around navigation, frames, and streams, while htmx uses declarative HTML attributes to issue requests and swap returned HTML into a target.

I maintain HTMX.NET, an optional ASP.NET Core enhancement library that helps integrate htmx with server-side code. Start with the official htmx documentation, then see this site’s introductions to htmx with ASP.NET Core, htmx swapping techniques, and anti-forgery tokens in htmx requests.

If your team prefers a React-like component model but wants to build components in C#, ASP.NET Core ships with Blazor. Blazor supports static server-side rendering plus interactive server, WebAssembly, and auto render modes. It is a different architectural choice from controller-and-template applications, so choose it when stateful, reusable, interactive components are a primary part of the user experience rather than as a direct substitute for Turbo.

You can also combine these approaches where it makes sense. For example, Blazor server-rendered components can work with htmx when you want reusable components without committing every part of the UI to a single client interaction model.

What Is the ASP.NET Core Equivalent of Rails API Mode?

If you’re a Rails developer primarily building an API for a frontend, mobile client, or another service, ASP.NET Core offers two primary approaches: API controllers and Minimal APIs. Both use the same routing, model binding, validation, authentication, authorization, dependency injection, and JSON serialization features. The difference is primarily in how your application organizes endpoints.

Choose API controllers for larger APIs that benefit from established conventions, shared filters, and a familiar class-and-action structure. Choose Minimal APIs for smaller services, focused endpoint groups, or teams that prefer defining an endpoint close to the code that handles it. Neither approach is more capable by default; the right choice is the one your team can keep consistent as the application grows.

Here is a practical translation of common Rails API concepts:

Rails API mode ASP.NET Core
config/routes.rb Endpoint route configuration in Program.cs, route groups, or controller route attributes
Controller action Controller action or Minimal API route handler
Strong parameters Request models, model binding, and validation
render json: Returning a result or typed response serialized as JSON
before_action Middleware, endpoint filters, MVC filters, and authorization policies
Jbuilder or serializers Response DTOs, anonymous projections, or dedicated serialization types

Avoid returning Entity Framework Core entities directly from API endpoints. Instead, accept request DTOs and return response DTOs or projections that describe the public API contract. This prevents database implementation details, navigation properties, and accidental fields from leaking into responses while giving you room to change persistence without breaking clients. For deeper Minimal API patterns, see endpoint filters in ASP.NET Core and generating HTTP endpoints with Minimal APIs.

The official ASP.NET Core Web API documentation and Minimal APIs quick reference cover the framework features in detail.

What Is the .NET Equivalent of Active Record?

Rails developers typically use Active Record, an object-relational mapper built around conventions and active record objects. Entity Framework Core serves a similar purpose in .NET: it maps .NET objects to relational data, tracks changes, manages relationships, and supports schema migrations. The two tools solve many of the same problems, but they organize persistence differently.

The important distinction is that EF Core generally follows a Data Mapper style. Instead of putting persistence methods directly on a Post object, you use a DbContext to query and save entity instances. The DbContext represents a session with the database, tracks changes to loaded entities, and coordinates SaveChanges calls. This can feel less magical than Active Record, but it makes the data-access boundary more explicit.

Rails Entity Framework Core
Active Record model Entity class plus DbContext
rails db:migrate dotnet ef database update
Migration files EF Core migrations
Associations Navigation properties and relationship configuration
Scopes LINQ queries, extension methods, or query objects
Callbacks Explicit application/domain logic or EF Core interceptors when appropriate

LINQ is EF Core’s query language. Many LINQ expressions are translated to SQL and executed by the database, but not every .NET method can be translated. Learn to inspect generated SQL and project only the fields an endpoint needs. When loading relationships, be deliberate about eager loading with Include, projections, or separate queries so you do not introduce an N+1 query problem. For hands-on follow-up, see using Entity Framework Core in ASP.NET Core and adding EF Core migrations to .NET Aspire solutions.

The EF Core documentation and EF Core migrations documentation cover the underlying concepts and commands.

How Do Rails Generators and Bundler Map to .NET?

Rails has a famously cohesive command-line experience, particularly around generators and project conventions. The .NET CLI is strong at creating projects, building, testing, running, publishing, managing packages, and invoking tools, but it is less generator-centric. You will find scaffolding templates and third-party generators, but they are not as central to the day-to-day .NET workflow as rails generate is to Rails.

An uncomfortable truth for command-line-first Rails developers is that .NET developers often prefer IDE or editor tooling for discovery, refactoring, debugging, scaffolding, and project management. JetBrains Rider, Visual Studio, and Visual Studio Code with the C# Dev Kit are common parts of the workflow. You can build and ship entirely from the terminal, but the broader ecosystem often assumes you have capable editor tooling available, which may feel like a cultural adjustment when moving between the two communities.

Here are the most useful command-line translations:

Rails .NET
rails new dotnet new
bundle add dotnet add package
bundle exec dotnet tool run or a direct dotnet command
rails db:migrate dotnet ef database update
rails test dotnet test
rails server dotnet run or dotnet watch

NuGet is the closest equivalent to RubyGems, but a .csproj file generally holds package references rather than a separate Gemfile. Larger .NET solutions may also use Central Package Management to declare package versions in one Directory.Packages.props file.

Learn more in the official documentation for the .NET CLI and NuGet package management.

What Is the .NET Equivalent of ActiveJob and Sidekiq?

Background work is essential whenever an operation should outlive an HTTP request: sending email, processing uploaded files, generating reports, or synchronizing with another service. Rails developers commonly use ActiveJob as an abstraction and Sidekiq as a durable job processor. .NET has no single built-in ActiveJob equivalent, so applications usually target a chosen job library directly or define an application-specific abstraction around it.

The right .NET choice depends on whether the work must survive process restarts and whether you are primarily queueing work or scheduling it:

  • BackgroundService is built into ASP.NET Core and works well for continuous, in-process work such as polling a source, consuming an already-durable queue, or running a periodic task. By itself, it does not provide Sidekiq-like durable job storage, retries, or a dashboard. Treat it as part of your application process, not as a reliable replacement for a job queue.
  • Hangfire is the closest of these options to a Sidekiq-style job system. It persists background jobs in supported storage, supports retries and recurring jobs, and includes a dashboard. It is a strong choice when application code needs to enqueue durable work and monitor it.
  • Quartz.NET is primarily a scheduler. It is well suited for recurring and calendar-based work, and can persist schedules and jobs when configured with durable storage. Use it when scheduling is the central problem rather than general-purpose background job processing.

For work that must not be lost, use durable storage or a message broker and design for retries, idempotency, observability, and failure handling. An in-memory queue or a fire-and-forget task can disappear when an application restarts or scales out.

How Does the .NET Open-Source Ecosystem Compare to RubyGems?

Rails developers are accustomed to a deep ecosystem of gems, where reaching for a well-known community package is a normal part of building an application. The .NET ecosystem has a different center of gravity. ASP.NET Core and .NET include many capabilities that Rails developers might otherwise expect to add through gems: dependency injection, configuration, logging, authentication and authorization primitives, background hosted services, caching, HTTP clients, health checks, and structured JSON serialization.

This means you may need fewer third-party packages to get an application running, but it also changes where you look when you need a capability. Start with the .NET and ASP.NET Core documentation to understand what is already available, then search NuGet for a package when the platform does not meet your requirements.

The open-source ecosystem is active, but it is less centralized around a small set of universally adopted gems. You will find excellent projects, commercial products, Microsoft-supported packages, and multiple competing options for common problems. Evaluate them the same way you would a Ruby gem: check maintenance activity, issue response, release cadence, documentation, security posture, licensing, and whether the project fits your deployment model.

The practical adjustment for Rails developers is not that .NET lacks open source. It is that the platform itself covers more of the common web-application baseline, while third-party choices are often more fragmented and problem-specific.

Rails to .NET Cheat Sheet

Rails Closest .NET counterpart Notes
Action Pack ASP.NET Core MVC or Razor Pages MVC is the closest controller-and-view analogue.
Action View Razor Partial views, View Components, and tag helpers compose reusable UI.
Hotwire/Turbo htmx Both favor server-rendered HTML; their navigation and update models differ.
Active Record Entity Framework Core EF Core uses DbContext and LINQ rather than persistence methods on each model.
ActiveJob/Sidekiq Hangfire, Quartz.NET, or BackgroundService Choose durable storage for jobs that must survive restarts.
Rails CLI/Bundler .NET CLI/NuGet .NET relies more heavily on editor and IDE tooling.

Conclusion

Rails and .NET share more ideas than their languages and communities suggest at a cursory glance. Rails offers a strongly opinionated path from a new application to a working product. ASP.NET Core offers familiar web-development building blocks with more opportunities for a team to decide how those blocks fit together.

If you want to try the .NET ecosystem, start small: create an ASP.NET Core MVC or Razor Pages application, build one CRUD feature with EF Core migrations, and choose htmx or Blazor only when the user experience calls for it. That exercise will show you where the frameworks overlap, where their tradeoffs differ, and which conventions your team needs to establish.

This map is a starting point, not a migration checklist. It began with a question on r/dotnet asking for Rails equivalents, and I hope it gives Rails developers enough context to explore .NET with fewer surprises. Bring the Rails practices that serve your team well, learn the .NET platform conventions, and make intentional choices where ASP.NET Core leaves room for them.

Read the whole story
alvinashcraft
1 hour ago
reply
Pennsylvania, USA
Share this story
Delete

Open Source, a relic, a charity or still the thing?

1 Share

Collection of vintage items on shelves and floor - Free Photo on Unsplash

A friend asked me recently whether there is any sense in doing Open Source now. My answer was the same as always. I see three options.

1. You get money out of it.
2. You’re dogfooding it in your work projects.
3. You have fun doing it.

If none of that is true, it makes no sense and never did. Best if there’s a combination of 1 or 2 with 3. And at least one of those options is a bare minimum.

Doing OSS is mostly for fun; success stories of single people who succeeded financially through it are rare. Personally, even though some of my projects got wide adoption, I always struggled to make money from them to justify the effort. Unfortunately, OSS creators don’t get enough respect for their effort; with GenAI, that’s even lower. And I don’t think that’s good for the industry long-term, but that’s, in my opinion, the reality.

So my advice would be to think about whether:

1. Your project has commercial potential.
2. It can be turned into a product.
3. There are people willing to pay for that.

If yes, go closed-source and productise it. You can always open it later or make the source available to customers.

This kind of goes back to the old days of shareware, etc. I don’t like that, but this is how it goes. I also need to learn from my own advice.

„I’m just standing on the shoulders of giants”

is a common phrase to show that you’re humble.

In the Open Source, it is the opposite. In our industry, giant companies stand on the shoulders of a small group of passionate, regular folks.

Many people believe that “someone is sponsoring those projects”, but when you think about “ok, but did I sponsor some?” then the answer is usually no.

I have the luck to get some sponsorship. Not huge, but still. I’m calling that “Whiskey money”. I don’t drink a lot, but I could buy a bottle with that amount from time to time and could salute those good folks.

It’s important to think of supporting OSS as due diligence, not gratitude. It’s ensuring that critical parts of your architecture will be maintained and continued. Essentially, that lowers your cost and risk.

Yes, we can vibe code a lot these days, but do we want to take responsibility and get late-night calls if that part doesn’t work?

So folks, check whether the OSS you’re using has support contracts, consulting or workshops, sponsorship, or paid add-ons. Don’t think of them as charity, but as a way to outsource work someone else can do cheaper and more reliably.

Read also my older takes:

Cheers!

Oskar

p.s. If you like my work, use my tools, and it helped you, you can do that through my GitHub sponsorship. I’ll salute you in the evening time! https://github.com/sponsors/event-driven-io

p.s. Ukraine is still under brutal Russian invasion. A lot of Ukrainian people are hurt, without shelter and need help. You can help in various ways, for instance, directly helping refugees, spreading awareness, putting pressure on your local government or companies. You can also support Ukraine by donating e.g. to Red Cross, Ukraine humanitarian organisation or donate Ambulances for Ukraine.

Read the whole story
alvinashcraft
2 hours ago
reply
Pennsylvania, USA
Share this story
Delete

UWP and WinUI 3 apps: UI testing with MSTest

1 Share

Reliable testing of UWP and WinUI 3 apps needs the app’s real UI dispatcher, not just a single-threaded apartment (STA) thread, so that test setup, async test code, and cleanup all run with UI-thread access. A WinUI 3 test still looks this simple:

[UITestMethod]
public async Task GridCanBeCreatedOnTheUIThread()
{
    await Task.Yield();

    var grid = new Grid();

    Assert.IsTrue(grid.DispatcherQueue.HasThreadAccess);
}

With MSTest 4.5 and Microsoft.Testing.Platform (MTP) 2.5, you can use the same UI-thread testing pattern for UWP and WinUI 3 apps. The supported models are classic and modern UWP, packaged or unpackaged WinUI 3, and WinUI hosts that use AppContainer. MSTest.Sdk picks the launch path for each model. It starts unpackaged apps directly and uses Microsoft.Testing.Extensions.PackagedApp to register and activate packaged apps by AUMID. For AppContainer hosts, it also grants the exact package SID access to the controller and report pipes. Packaged and sandboxed apps do not start as the initial test tool. MSTest.Sdk launches a normal full-trust sidecar controller that owns test arguments, cancel requests, reports, retries, and the final exit code. It then starts the app that hosts the tests. No Microsoft.NET.Test.Sdk, vstest.console, UwpTestHostRuntimeProvider, or Visual Studio deployment runtime is used.

For more background, read the introduction to MSTest.Sdk and the overview of Microsoft.Testing.Platform support across .NET test frameworks.

Packaged activation needs machine setup

To register an unsigned build-output layout, you need Developer Mode or a similar sideloading policy. Therefore, run AppContainer tests non-elevated, and confirm the policy on your CI agent, not just your workstation.

Choose the model for UWP and WinUI 3 apps

Packaging and sandboxing are separate choices. Packaging adds MSIX identity and AUMID activation; the trust level decides whether the process is full trust or runs in AppContainer.

Application models for UWP and WinUI 3 apps select direct startup or package registration and AUMID activation

Application model Identity and trust MTP test-host path
Classic UWP (uap10.0) MSIX, AppContainer Sidecar controller, UAP adapter/bootstrap assets, AUMID activation
Modern UWP (UseUwp) MSIX, AppContainer Sidecar controller, Native AOT host, AUMID activation
Unpackaged WinUI 3 No package identity, full trust Direct apphost launch
Packaged WinUI 3 MSIX, full trust Sidecar controller, package registration, AUMID activation
WinUI 3 packagedClassicApp with AppContainer trust MSIX, AppContainer Sidecar controller and exact package-SID pipe authorization

For WinUI, start unpackaged unless the behavior under test needs package identity, packaged activation contracts, or an exact match with installed app behavior. In contrast, UWP is inherently packaged and sandboxed.

Select MSTest.Sdk and MTP for UWP and WinUI 3 apps

At the solution or repo root, use global.json to pin MSTest.Sdk 4.5 and select Microsoft.Testing.Platform for the native .NET 10 dotnet test experience:

{
  "test": {
    "runner": "Microsoft.Testing.Platform"
  },
  "msbuild-sdks": {
    "MSTest.Sdk": "4.5.0"
  }
}

Otherwise, .NET 10 uses VSTest for dotnet test. MSTest.Sdk 4.5 includes MTP 2.5, the app-model sidecar controller, UWP adapter/bootstrap assets, and the packaged launcher.

Configure UWP and WinUI 3 apps

Modern UWP

For a modern UWP project, the test setup is small:

<Project Sdk="MSTest.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
    <UseUwp>true</UseUwp>
    <PublishAot>true</PublishAot>
  </PropertyGroup>
</Project>

Keep the app’s XAML, manifest, architecture, and Native AOT settings. Then, from OnLaunched, pass the activation string to the generated MTP helper:

using Microsoft.Testing.Extensions;

protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
    Window.Current.Activate();
    string[] testArguments =
        PackagedAppExtensions.GetTestApplicationArguments(args.Arguments);
    Environment.ExitCode =
        await MicrosoftTestingPlatformApplication.RunAsync(testArguments);
    Exit();
}

Classic UWP

Classic uap10.0 projects keep their existing UWP project structure and import MSTest.Sdk alongside MSBuild.Sdk.Extras. MSTest 4.5 includes the UAP-compatible adapter, generated bootstrap, packaged-app launcher, and TRX client assets. The sidecar builds the .build.appxrecipe layout, installs its declared frameworks, and starts the app by AUMID.

Classic and modern UWP builds still need the Visual Studio MSBuild/UWP toolchain. However, they no longer need its VSTest runtime or deployment provider. See the UWP and WinUI testing guide for the complete classic project imports and package layout.

Therefore, run UWP tests from a Developer PowerShell for Visual Studio. Build with the desktop MSBuild toolchain, then invoke the MTP target:

msbuild .\MyUwpTests.sln /restore /p:Configuration=Release /p:Platform=x64
msbuild .\MyUwpTests.csproj /t:InvokeTestingPlatform /p:Configuration=Release /p:Platform=x64

AppContainer-configured WinUI 3

A packaged WinUI 3 app is full trust by default. For example, to test a supported packagedClassicApp AppContainer setup, keep the packaged WinUI host shown below and set uap10:TrustLevel="appContainer" in the manifest. The host still receives normal process arguments, while MTP grants only that package SID access to its controller, cancellation, TRX, HangDump, and Retry pipes. Do not grant ALL APPLICATION PACKAGES.

Then, run this AppContainer shape from a non-elevated Developer PowerShell through the MTP MSBuild target:

dotnet build .\MyAppContainerTests.csproj -c Release -p:Platform=x64
dotnet msbuild .\MyAppContainerTests.csproj /t:InvokeTestingPlatform /p:Configuration=Release /p:Platform=x64

Build the self-hosted WinUI 3 test app

The following complete WinUI 3 test app supplies that dispatcher and runs MSTest inside the app itself. Build this shared host once; the packaged and unpackaged deltas come afterward. Three pieces make up that shared project:

The csproj configures the MSTest runner; UnitTestApp hosts the platform; test classes run on its published dispatcher

Configure the shared project

You need Windows, the .NET 10 SDK, and the Windows App SDK tooling (installed by Visual Studio’s “Windows application development” workload, or restored from NuGet if you build from the CLI). Then, start from Visual Studio’s “Blank App, Packaged (WinUI 3 in Desktop)” template, retain its Package.appxmanifest and package assets for the packaged form, and apply this shared project setup.

<Project Sdk="MSTest.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0-windows10.0.19041.0</TargetFramework>
    <TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
    <UseWinUI>true</UseWinUI>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <Page Remove="UnitTestApp.xaml" />
    <ApplicationDefinition Include="UnitTestApp.xaml" />
    <ProjectCapability Include="TestContainer" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.WindowsAppSDK"
                      Version="1.8.251106002" />
  </ItemGroup>
</Project>

Define the application entry point

The ApplicationDefinition points to this minimal UnitTestApp.xaml:

<Application
    x:Class="MyWinUiTests.UnitTestApp"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Application.Resources />
</Application>

Host the test run from OnLaunched

The WinUI app owns its entry point through this ApplicationDefinition. MSTest.Sdk detects that entry point, suppresses its own competing Main, and generates a reusable MicrosoftTestingPlatformApplication.RunAsync helper. Finally, the app’s code-behind creates the window, publishes its dispatcher, and calls that helper:

using Microsoft.UI.Xaml;
using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;

namespace MyWinUiTests;

public partial class UnitTestApp : Application
{
    private Window? _window;

    public UnitTestApp() => InitializeComponent();

    protected override async void OnLaunched(LaunchActivatedEventArgs args)
    {
        _window = new Window();
        _window.Activate();
        UITestMethodAttribute.DispatcherQueue = _window.DispatcherQueue;

        try
        {
            Environment.ExitCode = await MicrosoftTestingPlatformApplication.RunAsync(Environment.GetCommandLineArgs()[1..]);
        }
        finally
        {
            _window.Close();
            Exit();
        }
    }
}

That single OnLaunched override plays out in this order every time the app starts, whether launched directly or activated by AUMID:

sequenceDiagram
    participant OS as Windows
    participant App as UnitTestApp.OnLaunched
    participant MTP as Microsoft.Testing.Platform
    participant Tests as MSTest UITestMethod tests

    OS->>App: Launch (apphost or AUMID activation)
    App->>App: Create and activate the Window
    App->>Tests: Publish UITestMethodAttribute.DispatcherQueue
    App->>MTP: MicrosoftTestingPlatformApplication.RunAsync
    MTP->>Tests: Run tests on the published dispatcher
    MTP-->>App: Test-run result
    App->>App: Environment.ExitCode = result
    App->>OS: Window.Close + Exit

Assigning Environment.ExitCode is important: a WinUI-generated entry point returns void, so without it a failing test run can look successful to a build or CI system. MSTest.Sdk owns MSTest references, extension registration, and the packaged-app launcher registration. The WinUI testing guide covers the generated helper and hosting model in more depth.

Verify the dispatcher in a UI test

The app and tests now form one host. For example, this async test checks that TestInitialize, the test body, and TestCleanup keep access to the UI dispatcher:

using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Controls;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace MyWinUiTests;

[TestClass]
public sealed class ViewTests
{
    private bool _initializedOnUiThread;
    private bool _verifyCleanupOnUiThread;

    [TestInitialize]
    public async Task InitializeAsync()
    {
        await Task.Yield();
        _initializedOnUiThread =
            DispatcherQueue.GetForCurrentThread()?.HasThreadAccess == true;
    }

    [TestCleanup]
    public async Task CleanupAsync()
    {
        await Task.Yield();

        if (_verifyCleanupOnUiThread)
        {
            Assert.IsTrue(
                DispatcherQueue.GetForCurrentThread()?.HasThreadAccess == true);
        }
    }

    [UITestMethod]
    public async Task ControlCanBeCreatedAfterAsyncInitialization()
    {
        _verifyCleanupOnUiThread = true;
        await Task.Yield();

        var grid = new Grid();

        Assert.IsTrue(_initializedOnUiThread);
        Assert.IsTrue(grid.DispatcherQueue.HasThreadAccess);
    }
}

[STATestMethod] can provide an STA thread, but it does not create a WinUI dispatcher. [UITestMethod] dispatches the full MSTest call for each test, including its setup and cleanup.

Apply deployment models to UWP and WinUI 3 apps

For UWP and WinUI 3 apps, apply the delta that matches the deployment model you chose in “Choose the model for UWP and WinUI 3 apps.” MSTest.Sdk keeps the runner setup shared between both models.

Unpackaged WinUI 3 (default choice)

Add these properties and do not include MSIX manifest or package asset items:

<PropertyGroup>
  <WindowsPackageType>None</WindowsPackageType>
  <EnableMsixTooling>false</EnableMsixTooling>
</PropertyGroup>

The resulting apphost is a standard executable. As a result, Microsoft.Testing.Platform uses its normal launch path, and this route does not use VSTest’s appx runtime provider.

Packaged full-trust WinUI 3 (when you need identity)

Remove the two unpackaged overrides; do not leave <WindowsPackageType>None</WindowsPackageType> in the project. Retain the template’s normal Package.appxmanifest and package assets. The output then has MSIX identity, so Windows must register the package layout and activate the test host by AUMID. It needs a Windows target framework moniker (TFM) at 10.0.19041.0 or later and Developer Mode or a similar sideloading policy for unsigned build output.

In addition, MSTest.Sdk adds and registers the packaged-app launcher for packaged WinUI projects. Leave TESTINGPLATFORM_PACKAGEDAPP_LAUNCHER unset. Its default, auto, enables the packaged launch path only when a matching AppxManifest.xml describes the app. Otherwise it keeps the normal, faster launch path with no controller restart or deployment-copy overhead. The WinUI testing guide covers the always and never overrides for less common scenarios.

Validate on your CI agent first

Confirm Developer Mode (or your sideloading policy) and a clean, passing exit code on your own packaged app and CI agent, as covered in “Packaged activation needs machine setup” above, before wiring this into a required gate.

Run it

Use dotnet run for either deployment model. It launches the generated apphost, which hosts Microsoft.Testing.Platform inside the process that owns the window and dispatcher:

dotnet run

The .NET 10 native MTP runner also supports dotnet test for either deployment model:

dotnet test --project .\MyWinUiTests.csproj -c Release -a x64

For an unpackaged app, you can also launch the generated apphost directly. However, do not use dotnet exec, which puts dotnet.exe in the middle and can break WinUI resource loading.

A window flashes briefly while the test runs, and the console reports a summary:

Passed! - Failed: 0, Passed: 1, Skipped: 0, Total: 1, Duration: 63ms - MyWinUiTests.dll (net10.0-windows10.0.19041.0)

The packaged development layout can remain registered after the run. Remove it when needed by using its manifest Identity name:

Get-AppxPackage -Name '<package identity name>' |
  Remove-AppxPackage -PreserveApplicationData

Validate UWP and WinUI 3 apps on the target machine

For a packaged test host, a passing build is only the first step. Therefore, check your actual CI agent image, not just a developer workstation, for a user context that can register the package, the required Developer Mode or sideloading policy, and the frameworks declared by the package. Otherwise, a workstation that already has the UWP framework packages or Windows App SDK runtime installed can hide a gap that only appears on a clean build agent.

Framework-dependent WinUI 3 apps need the matching Windows App SDK runtime on the agent. A self-contained WinUI 3 build can remove that machine need, but test it using the exact package model that will run in CI.

Start testing UWP and WinUI 3 apps

Use this checklist to start testing UWP and WinUI 3 apps:

  1. Select Microsoft.Testing.Platform in global.json and use MSTest.Sdk 4.5.
  2. For modern UWP, set UseUwp and PublishAot. For classic UWP, import MSTest.Sdk into the existing project. For WinUI 3, set UseWinUI and choose packaged or unpackaged deployment.
  3. Call the generated MicrosoftTestingPlatformApplication.RunAsync helper from OnLaunched. Modern UWP restores args.Arguments through PackagedAppExtensions.GetTestApplicationArguments; WinUI uses process arguments. Publish the UI dispatcher where required.
  4. Run full-trust WinUI with dotnet run or dotnet test. Run UWP and AppContainer WinUI through the InvokeTestingPlatform MSBuild target from a non-elevated Developer PowerShell.
  5. For packaged and AppContainer models, confirm Developer Mode (or your sideloading policy) and run non-elevated on the test agent.

One test platform for UWP and WinUI 3 apps

Keep MSTest.Sdk, the generated MTP helper, and [UITestMethod] across the Windows app models. Let the SDK choose direct startup for unpackaged WinUI or the sidecar controller for package registration, AUMID activation, and AppContainer isolation.

Three takeaways to carry forward:

  • Reuse the same MSTest lifecycle and UI-dispatcher tests across UWP and WinUI 3.
  • Use direct apphost startup for unpackaged WinUI; let MTP register and AUMID-activate packaged or AppContainer hosts.
  • Validate the exact package model, required frameworks, trust level, and machine policy that your CI agents will run.

For more detail, see the UWP and WinUI testing guide and the MSTest documentation.

The post UWP and WinUI 3 apps: UI testing with MSTest appeared first on .NET Blog.

Read the whole story
alvinashcraft
2 hours ago
reply
Pennsylvania, USA
Share this story
Delete

Meet Nub: A Bun-like TypeScript Toolkit, Running on Node.js

1 Share

Nub is a comprehensive JavaScript/TypeScript toolkit that brings Node.js developer experience closer to Deno or Bun. This post is an overview of Nub and the various ways you can use it.

I still have a few projects that use Bun, and I’ve written about using Bun as a runtime or tool. For a demo, a small internal tool or a one-off script, Bun let me write TypeScript and run it immediately. I did not need to choose between ts-node and tsx, set up TypeScript tooling or make a custom loader. Its package manager was also faster than npm, and its built-in test API meant I did not have to bring in Vitest for every small project.

The appeal was simple: fewer tools to configure before I could start writing code, and easy project tooling.

But most of those projects are not really Bun applications. They do not depend on Bun.serve(), Bun.file() or other Bun-specific APIs1. Bun is mainly the command I type to run or manage TypeScript projects.

That distinction matters, because Node.js has changed a lot.

Node.js Now Has More Modern Features

I previously wrote about Node.js gaining built-in support for environment files, watch mode and running package.json scripts. I also covered its early support for running TypeScript files directly and native SQLite bindings.

The built-in test runner through node:test was also an exciting one for me. For many of my projects, that removes one more reason to reach for another runtime or install a separate test framework.

Still, these features do not quite add up to a complete TypeScript project experience.

Node’s built-in TypeScript support is intentionally limited. It can remove types from a file and run the remaining JavaScript, but it does not read your tsconfig.json. It does not handle JSX, and TypeScript features that need to be converted into JavaScript require more than simple type removal. Import paths that work in your editor can also fail when the program starts.

That is a sensible boundary for Node.js. It is careful about changing how programs run, and that caution is part of why it is trusted in production.

The result, however, is that a normal TypeScript project can still collect several tools around Node: a TypeScript runner (e.g., tsx), a file watcher, a package manager, a script runner and/or a Node version manager. In my case, I might use pnpm for dependencies, n for selecting a Node version and another package for running TypeScript.

Each tool is useful. The combined setup is what I dislike.

Nub Is Not Another JavaScript Runtime

Nub is a comprehensive command-line toolkit for Node.js. It is written in Rust, but your application still runs on the normal Node.js runtime. It includes a script and file runner, package manager and Node version manager.

“How does it do that?” you may ask.

Nub prepares the TypeScript files, applies the project settings it needs, selects the correct Node.js version, loads the environment files and then hands the application (transpiled files) to Node.js. It works for both JavaScript and TypeScript projects and files.

You can install it using the command npm install --global @nubjs/nub, or brew install nubjs/tap/nub in Homebrew. There are other installation options which you’ll find in their documentation.

Here are the major commands that show most of its purpose:

nub scripts/migrate-db.ts
nub watch src/server.ts
nub run test
nubx prisma generate
nub install

The first command runs a TypeScript file directly. Unlike Node’s built-in type stripping, Nub supports the wider set of TypeScript features used in real projects. It can read path aliases from tsconfig.json, handle TSX and decorators, and make imports behave more like they do in the editor. The nubx is the alternative to npx or pnpx.

AFAIK Nub does not type-check your program. It prepares the file for execution. I would still keep TypeScript checking in development and CI:

tsc --noEmit

With tsc moving to Go, you’re assured that your CI remains fast.

Nub as a Node.js Utility

Running a TypeScript file is the clearest introduction to Nub, but the more interesting part is how it treats the whole project. The file watcher nub watch restarts the program when its code changes. It can also react to project files such as tsconfig.json, package.json and loaded environment files. This is more useful than watching a directory without understanding which files affect the running application.

The nubx command runs binary tools such as Prisma, ESLint or tsc. It checks the local project first. When a package is missing, Nub asks before downloading and running it. In CI, it will not quietly fetch a package unless that behavior was explicitly allowed.

It can also manage the Node version used by a project. It reads common sources such as .nvmrc, .node-version and the engines.node field in package.json. If the requested version is not available, Nub can download and cache the official Node.js build.

Nub may seem boring if you’re used to Bun, but may be a bigger benefit than it first appears. A new contributor can clone a repository and run the project with its intended Node version without first learning which version manager the team uses. CI and local development are also less likely to drift onto different versions.

Together, these features make Nub feel less like a replacement for tsx or pnpm, and more like having a 5x developer experience in Node.js.

You Can Keep pnpm

Or not.

Nub includes package-management features, but adopting Nub does not require replacing your existing package manager. For non-Bun projects, I normally use pnpm. I can continue using it for installation and lockfiles while using Nub to run TypeScript files, package scripts, local commands and the correct Node version.

However, I can switch to Nub for package management because Nub can read and write pnpm version-9 lockfile! There is just a tiny set of pnpm features that Nub can’t replace for pnpm (yet!), for example, pnpm deploy. If I don’t use such a feature, it becomes an easy switch.

There are various ways Nub supports you in working with or migrating off other package managers like npm, Bun and yarn. That makes Nub easier to try, and it shows the team took time to make it easy to work with. So do check out their documentation.

Could Nub Replace Bun in My Projects?

For some of them, yes.

The best candidates are projects where Bun is mainly being used for direct TypeScript execution, testing or package management. Those needs can now be covered by Nub and Node’s built-in test runner. This also means development and production can use the same runtime, and that matters on serious projects. Bun has made some progress on Node.js compatibility, but it is still a different runtime with incomplete Node compatibility. Many packages work, but less common behavior and native add-ons can still expose differences.

Node 25.8 at 100%, Nub at 98.8%, Deno at 2.8 77.4%, Bun at 1.3.14 at 40.5%
Image from Nub website

Nub avoids that class of mismatch because it does not imitate Node.js. It starts the Node.js process.

That does not make Nub a drop-in replacement for every Bun project. An application using Bun.serve(), Bun.file(), bun:sqlite, bun:test or other Bun-specific features has made a real runtime choice. Moving it to Node requires replacing those APIs, not merely changing the command in the terminal. Nub’s Bun migration guide has a migration guide for such a scenario.

So the practical question is not, “Can Nub replace Bun?” It is, “Why is this project using Bun? And do I really need the Bun-specific API?”

A Small Way to Start

You could test Nub on a low-risk part of an existing project if you’re curious to try it. It could be on a maintenance script, a small demo or as a Node version manager. That way you can keep the experiment small and reversible because Nub does not need to take over the whole project before it can be useful.

The interesting thing about Nub is not that it replaces Node.js, unlike Bun or Deno. It is that it makes the TypeScript-in-Node.js experience easy, and replaces a lot of distinct tools around managing the project. That is why it has made me reconsider my remaining Bun projects.

Give it a try and let me know how it works out for you.


  1. You might be surprised given the runtime performance marketing for Bun. However, the Node-API compatibility and occasional stability issues meant I had to go back to Node.js for serious work, and many legacy projects still relied on Node.js compatibility. And many personal projects ran on Cloudflare Workers. ↩︎

Read the whole story
alvinashcraft
2 hours ago
reply
Pennsylvania, USA
Share this story
Delete

The Power of Polish

1 Share

Polish isn’t aesthetics, it’s protection. Rough software corrodes test, tech and trust. Learn why polish is on purpose, not just an afterthought.

Say the word “polish” and people hear “aesthetics.” The final coating. Something you add once the real work is done.

I don’t see it that way. Polish serves a purpose.

Why We Polish Metal

Think about why we polish metal in the first place.

A rough surface needs treatment, otherwise it traps moisture and dirt. That’s to prevent the metal from corroding. A polished surface is smooth, safer to handle, easier to clean and therefore it will last longer.

We don’t polish metal to make it shine. The shine is just what happens along the way.

The real reason is protection.

Fidelity Is Not Quality

In software development, it is common to create designs with sketches. These are called prototypes or low-fidelity wireframes.

Low-fidelity means the design has minimal visual detail and is usually quick and easy to make.

But somewhere along the way we started treating fidelity and quality as the same thing. Fidelity should be how much you show. Quality is how well you show it.

Low fidelity became an excuse for low quality. Quick and dirty sketches with misaligned boxes and typos. “Don’t look at the details, it’s just a wireframe.”

But people always look at the details.

My wireframes were always pixel-perfect. Sounds like a contradiction, since wireframes are supposed to be quick and rough. But an unpolished wireframe corrodes your results.

Show someone rough work, and they react to the roughness. I noticed the reaction to these imperfections became the feedback. Entire meetings derailed because of a misspelled label or inconsistent spacing.

Feedback that has nothing to do with the concept you actually wanted to test.

Polish protects that concept. Low fidelity, high quality. To me that’s not a contradiction, but a way to get results you can trust.

What Rough Software Corrodes

When low fidelity leads to lowering quality standards, you get rough software.

Rough software has serious corrosion risk worth polishing for:

Test

Put rough work in front of someone and they respond to the roughness, not the idea.

Like I mentioned, I’ve sat in reviews where a broken button or an inconsistent label hijacked the whole conversation. Everyone had opinions about the mistake. Nobody talked about the concept underneath it.

The test doesn’t find the answers you were looking for.

Tech

Unpolished increments don’t stay small.

An inconsistency here, a shortcut there. Each one looks minor by itself. But rough edges compound as the system grows. A death by a thousand cuts.

What started as a small patch becomes the pattern everyone else builds on top of. Debt paid later, with interest, instead of avoided with a bit of attention.

Trust

Trustworthiness is the big reason we should care about quality. It isn’t only about what people can see.

One misaligned label, one inconsistent flow, and people start doubting. From the part they noticed to the parts they can’t. What about the logic then? If they don’t care about this, do they care about data security? It can bring into question everything under the hood they’re trusting you got right.

Polish is about product and perception protection, about being trusted.

Beyond Software

I see the same polish pattern outside of software.

When I build a business blueprint, low fidelity by design, or a simple core story on a slide, and when the quality is there—when it’s polished—the content and intent lands.

People receive and perceive it better. It’s easier to maintain. Easier to expand later.

I recently presented a core story and brand manifesto to a client. It was a well-written paragraph, shown on a polished slide. The client actually commented that the slide showed quality and it delivered the message more strongly. It resonated.

When people trust it more, they value it more.

Purposeful polish.

The Mirror Trap

There’s one catch: you could polish forever.

Keep polishing, and metal becomes a mirror. A mirror only reflects you.

At some point polish stops protecting your software and starts serving you, the maker. You’re admiring your own craft, instead of removing noise for the user.

Polish has a purpose. And that purpose defines the boundary.

Good enough for now isn’t an excuse to ship rough work. Good enough for now still means good. You just need to know when to stop.

Closure

Forget the gloss at the end. Polish is the protection on purpose.

So don’t confuse fidelity with quality.

Do what you do well, in increments. Make it good enough for now. But make it shine.

Read the whole story
alvinashcraft
2 hours ago
reply
Pennsylvania, USA
Share this story
Delete

Goodbye, Microsoft

1 Share

A few years ago I took a new job at Microsoft, working as a Senior Content Developer, and as of this writing I own all the SQL Server content in Microsoft Docs. Not bad. After the release of ChatGPT in 2022, it was only a matter of time before large language models (LLMs) spelled the… 

The post Goodbye, Microsoft appeared first on Born SQL.

The post Goodbye, Microsoft appeared first on SQLServerCentral.

Read the whole story
alvinashcraft
2 hours ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories