Jimmy Bogard: Recent Episodes

None

I'm Jimmy Bogard, a software architect and consultant. I created the OSS libraries AutoMapper, MediatR, and Respawn. I help teams build systems faster, better, and more maintainable.

View Details

Next month I'm hosting a free webinar on Vertical Slice Architecture, DDD, and how they fit in with AI-assisted development. I, like a lot of developers, have embraced AI assistance for designing and building systems. While these tools can accelerate the creation of code, they don't necessarily create malleable or maintainable code. This has real-world impacts on the systems we build - burning tokens on long review cycles, introducing bugs, or simply complexity, rather than value-add features. In this webinar, I'll show how VSA can shorten those cycles by reducing side-effects and coupling in your systems.

The webinar is next month (August 20th) and you can register for free here:

Vertical Slice Architecture: Effective Guardrails for AI Development

Hope to see you there!

View Details

Today we released the 16.2.0 version of AutoMapper and 14.2.0 version of MediatR:

  • AutoMapper Release Notes
  • MediatR Release Notes

This release is a bit more enterprise-focused, with extensions for setting the license keys via environment variables, fixing some threading issues around license key validation, and including more security-related items on releases (SBOMs, etc.)

These last few releases have been targeting items more or less required for enterprises consuming commercial packages. In the next release, we'll be more focused on adding features and fixing historical bugs.

  • AutoMapper NuGet
  • MediatR NuGet

Enjoy!

View Details

This release is a patch release to fix a thread deadlock and security issue. From the release notes:

Thread DeadlockThanks to @t0m-4 for reporting this issue, which due to Microsoft deprecating some of the "sync" APIs for decryption, led to potential thread starvation and locking issues. The update still has to use a "sync-over-async" pattern, but does so in a much safer manner.

SecurityWe fixed an issue where certain cyclic or self-referential object graphs could trigger uncontrolled recursion during mapping, potentially resulting in stack exhaustion and denial of service.

Applications that process untrusted or attacker-controlled object graphs through affected mapping paths may be impacted.

Users should upgrade to this release.

Security advisory: GHSA-rvv3-g6hj-g44x

Thanks to @skdishansachin for responsibly disclosing this issue.

View Details

Today I released AutoMapper 16.1 and MediatR 14.1 (as part of now regular quarterly releases):

  • AutoMapper Release Notes
  • MediatR Release Notes

AutoMapper added some interesting features, allowing for factories and conditions to use dependencies (previously only allowing Func-based callbacks. On the MediatR side, we added support for more interesting generic use cases in complex generic hierarchies as well as a number of bugs squashed.

You can get the latest drops on NuGet. And a little celebration is in order - AutoMapper hit 1 billion downloads 🥳

Enjoy!

View Details

With the release of .NET, we've released updated packages of AutoMapper and MediatR targeting .NET 10 (and all supported versions of .NET and 4.x of .NET Framework).

From this release forward, we're aligning to major release cadences of .NET since this also means upping our dependency versions as well. Minor versions will still happen throughout the year but upping dependencies often has just as many issues as breaking API changes so we want to align with that cadence to make it easier for folks to understand when things are changing.

This release doesn't have any API changes from the previous minor release, but we did add NuGet package signing:

NuGet package information with digital signaturesThis shows that the AutoMapper (and MediatR) packages were published by Lucky Penny Software (my company) and not...Unlucky Penny Software? Getting this additional security verification is important for customers to know that this package comes from us and not some other entity. Getting verified for code signing certificates is a fairly involved process doing things like "taking selfies with my passport".

Downloads and release notes below:

  • AutoMapper NuGet
  • AutoMapper v16.0.0 Release Notes
  • MediatR NuGet
  • MediatR v14.00 Release Notes

Enjoy!

View Details

Starting a new policy of regular quarterly releases, today I pushed out new versions of AutoMapper and MediatR:

  • AutoMapper 15.1.0 Release Notes
  • MediatR 13.1.0 Release Notes

The previous versions restored netstandard2.0 support across the board. In these releases, I'm including first-class support for net462 where I'm building and testing on both *nix and Windows. We noticed that just including netstandard2.0 as a target framework wasn't enough to really guarantee that the libraries would work against full .NET, so we now build and test on both.

There's some new features, enhancements, and bug fixes that you can view in the release notes above.

Enjoy!

View Details

One of my main goals of commercialization of AutoMapper and MediatR was being able to finally invest time in these projects where basically all new work stopped when I lost corporate sponsorship. I wanted to take some time to share where I'd like to take these projects now that I have that sponsorship back.

Tracking official .NET supportFirstly, the latest releases bring back netstandard2.0 support for both AutoMapper and MediatR which had dropped both years ago. MediatR was actually still on net6.0 prior to this release which was already out of support for months.

It wasn't exactly easy, especially because of how much net8.0 and net9.0 have diverged from netstandard2.0 not just in terms of APIs but C# language features, but having been part of several ASP.NET 4.x to ASP.NET Core migrations, having netstandard2.0 support makes this transition quite a bit easier. In the past we'd have to conditionally reference packages because there was no longer a common package version between say .NET 8 and .NET 4.8. That's something that I wish I had before that now I do.

AutoMapper RoadmapOne of the biggest complaints I hear about AutoMapper is that it's hard to debug - you're trading compile-time errors for runtime exceptions. We spent a LOT of time baking in better exception handling into the expression trees generated (resulting in worse performance, but better diagnostics), but that isn't always enough.

The answer here is source generators, but I'm not interested in merely copying other library's approaches. What I want to target is source generators that:

  • Plug in to AutoMapper's rich extensibility model
  • Stay true to AutoMapper's design philosophy
  • Support IQueryables (my favorite feature)
  • Track the features of AutoMapper's in-memory mapping
  • Support mapping validation (critical for any mapping tool)

Debuggability is my main focus here, although obviously performance would be a secondary win. Source generators have come a LONG way since I first looked at them when they were first released, so I'm excited to extend AutoMapper's functionality in this area.

This one is pretty big, so that's going to be my focus initially.

MediatR RoadmapSome folks have asked or even pointed to other libraries that do source generation of basically a copy of MediatR's API. I am looking at that, but there's been quite a few things on MediatR's backlog that I want to look at first. Source generation in mediators I find a bit less interesting in real-world projects, outside of philosophical debates.

MediatR is commonly used in concert with Vertical Slice Architecture, and a number of its features came out of using it in these scenarios (like behaviors). Today, a lot of features are tied in to the feature set of the stock Microsoft DI container. Unfortunately, features are only really added to that container if the ASP.NET Core team needs them. Even my PR to support generic constraints took like 5 years to merge in.

Moving away from relying on those DI features would mean I could do much more interesting things in the "application use case pipeline" that aren't possible with C#/DI alone, like:

  • Applying behaviors based on customized policies
  • Baking in support for result patterns
  • Direct support for application use cases
    • Blazor (sending a request from the client to a handler on the server)
    • Minimal APIs (scaffolding to separate API logic from application logic)
    • Domain events via notifications and EF/other ORMs

The idea of behaviors came from reviewing many production systems using MediatR and folding in that into first-class features. I am going to continue on this track.

What else are you interested in?

View Details

Today I'm excited to announce the official launch and release of the commercial editions of AutoMapper and MediatR. Both of these libraries have moved under their new corporate owner (me), Lucky Penny Software. I formed this company to house these projects separate from my consulting company, but it's just me there, I'm the sole corporate overlord.

The GitHub repositories have transferred to the new GitHub organization (along with their ownership) here:

  • LuckyPennySoftware/AutoMapper
  • LuckyPennySoftware/MediatR

With these, I've launched new home pages for each library:

  • https://automapper.io
  • https://mediatr.io

As well as a storefront site to purchase and manage licenses at:

  • https://luckypennysoftware.com

It's quite a bit to dig in to, so let's go over the details!

What's the new license?As discussed before, I wanted to release these libraries under a dual-license model:

  • Reciprocal Public License 1.5 (RPL1.5)
  • Lucky Penny Software Commercial License

It's a common dual-license model that many other OSS companies have chosen (MongoDB etc.) and had success with.

Under the commercial license, I've created a tier-based licensing model based on team size. There are no individual per-seat licenses, only licensing based on the number of developers.

How much will it cost?With a tier-based pricing approach, I wanted a pricing model that scales with team size and allows for company growth without a lot of hassle. There are 3 paid tiers:

  • Standard - 1-10 developers
  • Professional - 11-50 developers
  • Enterprise - Unlimited developers

Pricing is a subscription model, with both monthly and annual subscriptions (with a discount for annual subscriptions), as well as an option to bundle both libraries at a discount. You can find the details here (with all options), priced to your currency or in USD:

  • AutoMapper pricing
  • MediatR pricing

You can also find the details of what subscription benefits you'll get at the links above, including:

  • Private Discord channels
  • Priority support
  • Early access to new releases
  • Support for all currently supported versions of .NET Framework 4.x and .NET (netstandard2.0, net8.0, net9.0)
  • And more (as I build it)

All subscription payments are managed through Paddle, which supports...many different countries, currencies, and payment providers.

Do you have free licenses for ? Yes! Besides the RPL license, I'm also including a Community edition under the Commercial license that is free for:

  • Companies and individuals under $5,000,000 in gross annual revenue
  • Non-profits under $5,000,000 in annual total budget (expenditure)
  • Educational/classroom use
  • Non-production environments

You're still required to register for a license key, but this is only for auditing purposes.

How do I get the commercial versions?To make everyone's lives easier, these new major versions of AutoMapper and MediatR on NuGet are released under the new dual license agreement:

  • AutoMapper v15.0
  • MediatR v13.0

When you install these versions, you'll now be prompted for license acceptance. Once you obtain a license key, you'll be able to set the license key as:

services.AddAutoMapper(cfg => /* or AddMediatR */ cfg.LicenseKey = "<License key here>";}); I don't restrict usage of these products with a missing/invalid/expired license key, but you'll see some messages in your logs prompting you to supply a valid key.

What about the existing versions?I've created archived versions of the final releases of these two libraries:

  • AutoMapper/AutoMapper.Archive
  • jbogard/MediatR.Archive

Per those existing license agreements, you're free to fork, download, print out and read by the fireplace. Those archives will live on for anyone to use as they like.

If you're an existing user, you don't need to do anything. The existing NuGet packages (prior to the major versions listed above) are bound by the license agreements at the time of their release and will also live on.

Why Lucky Penny?Because she was my first dog! Although she's no longer with us anymore, I loved her spunk and her spirit and wanted to honor her memory with my company name (and logo). Here she is judging, always judging:

Penny the dogI named her Penny because 1) she was found by the side of a busy highway miles from anywhere (lucky for both of us) and 2) her copper color. So, Lucky Penny Software!

Lucky Penny Software logoIt's been a long journey to get here but I'm excited about what the future holds for these libraries that have amassed more than 1.1 billion downloads. Thanks everyone for your patience and support as I worked to launch!

View Details

In my last post, I shared the news that I've decided to take a commercialization route for AutoMapper and MediatR to ensure their long-term success. While that post was heavy on the motivation, it was intentionally light on the details. I did share that I wanted to be transparent on that process, and this post is part of that transparency.

There is a TON of information out there on possible models for sustainable open source, such as:

  • Consulting services
  • Open core
  • Hosted services
  • Dual license
  • a dozen others

Of course besides my previous situation, "be fortunate enough to work at a place that values and directly sponsors your work." This is the easiest place to be, but for projects that reach some threshold of users/downloads/complexity, maintainers must rely on sponsorship in some form or fashion. And when that sponsorship goes away for whatever reason, well, here we are.

Of the many options available, the most viable option is to move AutoMapper and MediatR to a dual license model.This looks to be the best choice after carefully examining the options and consulting with many other OSS maintainers who have already made this journey.

Dual Licensing ModelWhen I first started thinking about how I might go about this, I asked myself, "who bears the most responsibility in ensuring the sustainability of the OSS projects on which they depend?" which is a long winded way of saying "who should pay?" But another way of thinking of this is "who should NOT pay?" Looking at how others do this as well as how I want to approach it, I want to make these libraries free for:

  • Developers using it in an OSS setting
  • Individuals/students/hobbyists (using AutoMapper for fun not profit)
  • Non-profit/charities (maybe not for fun but also not for profit)
  • Startups or small companies (below some revenue/funding threshold)
  • Non-commercial setting (this I'm not sure is absolutely necessary with the other categories)
  • Non-production environments (instead of any trial period etc.)

I don't know if this exact verbiage is what will be the end result, but this is my overall goal.

Then for who I'm targeting for paid for-licenses, it's for-profit businesses using these libraries for commercial activities. Looking at my clients over the years who've used my libraries, it's a mix of these free/commercial categories.

In terms of a model for commercial licensing, I want to ensure that paid licenses add value beyond "I can download the license." This is the more fun part of this exercise for me, where I can try the things I never really could before without a more direct form of sponsorship/funding. I have a lot of ideas here, but nothing ready to share yet. If you have an idea of "if my company paid for a license, what else would I want to have included?" I would love to hear about it!

I am looking at a tiered license model but no per-seat licenses. I don't want to charge individual developers anything—that seems like a pain for everyone involved and I'm trying to keep things simple. "A new developer gets onboarded and now we need a new license" is too much for me to deal with and goes against the spirit of these libraries—the benefit is to the entire team, regardless of the number of developers.

I don't know what those tiers will be exactly, I'm figuring that out next. I do expect some blanket enterprise, site-wide licenses that hopefully makes everything simpler for everyone. I've been on the other side of the table, getting licenses approved internally with clients, and I understand predictability and simplicity go a long way.

Thoughts on PricingAs for pricing, I don't have details yet, and probably won't until launch in the next couple months. Range-wise, it's hard to compare to other commercial or dual-licensed products out there, since I don't want to do any individual or per-seat license and that seems to be the norm. I am however keenly aware of how much tooling and library products cost as I have to pay for many of these myself.

But if I were to compare to the cost for a team of 10 or 50 or 100 for their IDEs, I would expect my commercial license price to be a fraction of that.

Thanks again to everyone that's reached out with kind words and support, and to the community for their patience while I figure things out.

View Details

Yes, another one of "those posts". But tl;dr:

In order to ensure the long-term sustainability of my OSS projects, I will be commercializing AutoMapper and MediatR.

I did not post this on April 1st for obvious reasons. But first a little background on how I got to this point.

How I Got HereThese two projects originated at my time at Headspring, a consulting company I worked at for over 12 years. About 5 years ago, in January 2020, I decided to strike off on my own and give solo consulting a try. Although it was a scary leap, it's been more rewarding than I could have possibly hoped for, in almost every area.

The area that it didn't work out well, and not at all intentionally, was OSS work:

You can see exactly where my contributions cratered and flat-lined. And that's just commits—issues, PRs, discussions, all my time dried up. This wasn't the intention but was a natural side effect of me focusing on my consulting business.

At Headspring, my time on OSS was directly encouraged and sponsored by them. I could use time between projects to invest back in existing OSS or new OSS, because it benefited the client, the company, and the employees (me and my coworkers).

With me leaving that company, and that company then selling to Accenture later that year, I had no direct major sponsor of my OSS work anymore. My free time was being spent growing and ensuring the success of my consulting company, which being solo, is...kinda important.

Taking time to see how things have been going on all fronts, I had a bit of a shock looking at my OSS work. I realized that model is not sustainable for the long-term success of these projects, which I still endorse and believe in. I need to be able to pay for my time to work on these projects, and get direct feedback from paying clients, like I had earlier at Headspring.

What Will This Look Like?The short answer is "I don't know exactly". I'm working out those details now and will share them when I figure it out. I have lots of examples of what does and doesn't work well, at least from my perspective, as well as what I consider will work well for these projects.

Short term, nothing will change. I'll still be as (un)responsive on GitHub issues, and I just pushed out a couple releases of any existing work.

My goal is to be able to pay for the time to spend actually improving these projects, building out communities, helping more users, and in general, doing the things that people have asked me MANY times over the years that I should do, but I didn't, because it was not my job. OSS was/is/never will be a hobby for me. I want to change it to at least be part of my job and to fund real work.

I can’t rely on donations, I don't want to make developers pay anything or do anything to punish/annoy them, and I certainly don't think it's Microsoft's job to "pay me the money." Past that, I'm still figuring it out.

When Will This Happen?I don't know, it's still just me that owns everything. It's still using my free time to sort it out, as my day job is still a consultant. But I plan to be open with this whole process. I'm sure I'll surprise someone but the goal here is to be transparent.

Personally, I'm both filled with excitement and dread—doing these projects for so long has been incredibly rewarding, especially as this is code that came directly out of many, many long-lived production-deployed projects at Headspring. But I don't want these projects to wither and die on the vine, I want them to grow and evolve and thrive. But not just these projects—I want ALL my OSS projects (Respawn etc.) to thrive. This is how it needs to happen.

Final ThanksThanks to all that have contributed over the years, and especially to Lucian Bargaoanu who really helped pick up the torch with AutoMapper after I more or less fell off the map. Also thanks to my GitHub sponsors, as many a pint has been purchased with your generous support. And finally thanks to the community, I never hoped anything I built would help anyone beyond my clients, coworkers, and company, but it's always nice to hear that it has.

View Details

I pushed out MediatR 12.5 today:

  • Release Notes
  • NuGet

This is mainly a regular minor release with a couple extra interesting features:

  • Adding convenience method to register open behaviors
  • Better cancellation token support (it's passed now everywhere including behaviors)

And some other cleanup items as well. Enjoy!

View Details

I pushed out version 14.0 (!) of AutoMapper over the weekend:

  • Release notes
  • NuGet

This release targets .NET 8 (up from .NET 6 from the previous release). It's mainly a bug fix release, with some quality-of-life improvements in configuration validation where we gather up all the possible validation errors before reporting them in an aggregate exception.

Enjoy!

View Details

I've been playing around with Aspire for a bit mainly to understand "is this a thing I should care about?" and part of what I wanted to do is take a complex "hello world" distributed system and convert it to Aspire. Along the way, Particular Software also released container support for their Service Platform, so it also seemed like a good opportunity to try it out.

I'll follow up in another post about Aspire impressions, but the NServiceBus part was actually relatively simple. Many Aspire integrations have some kind of 1st-party support where you can do things like:

var rmqPassword = builder.AddParameter("messaging-password");var dbPassword = builder.AddParameter("db-password");var broker = builder.AddRabbitMQ(name: "broker", password: rmqPassword, port: 5672) .WithDataVolume() .WithManagementPlugin() .WithEndpoint("management", e => e.Port = 15672) .WithHealthCheck();var mongo = builder.AddMongoDB("mongo");var sql = builder.AddSqlServer("sql", password: dbPassword) .WithHealthCheck() .WithDataVolume() .AddDatabase("sqldata"); And now my system has RabbitMQ, MongoDB, and SQL Server up and running in containers. There's a lot of stock configuration going on behind AddSqlServer and similar methods but we don't have to use those convenience methods if we don't want to.

The overall Service Platform architecture looks something like:

The "instances" here are running containers that we need to configure in Aspire. On top of that, we might also want to have Service Pulse (another container) and Service Insight (a Windows-only WPF app) running, and these all require extra configuration. Also, the Error and Audit instances use RavenDB as their backing store but Particular also has an image there. The Docker Hub site has links to docs on both the instance and containers.

First up, we need to provide our license to the running containers as raw text in an environment variable, so we'll just read our license (this is just for local development):

var license = File.ReadAllText( Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ParticularSoftware", "license.xml")); Next, we need our RavenDB instance. There's a special image from Particular, so we'll use the AddContainer method to add our custom image to our Aspire distributed application:

builder .AddContainer("servicecontroldb", "particular/servicecontrol-ravendb", "latest") .WithBindMount("AppHost-servicecontroldb-data", "/opt/RavenDB/Server/RavenData") .WithEndpoint(8080, 8080); The container docs say that we must mount a persistent volume to that path, so we use the WithBindMount method to mount the volume following the Aspire docs.

Next up are the Particular containers!

Setting up the Service Control Error instanceFrom the Particular docs, we see that we need to supply configuration for:

  • Transport type (RabbitMQ, Azure Service Bus, etc.)
  • Connection string to the transport
  • Connection string to the Raven DB instance
  • Audit instance URLs
  • License

Plus port mapping. Pretty quickly I ran into a few challenges:

  • The Service Control image can start before RabbitMQ is "ready", resulting in connection failures
  • Service Insight, the WPF app, is Windows only so I need to connect to Service Control from a VM

The base configuration is fairly straightforward, we specify the container and image, with environment variables:

builder .AddContainer("servicecontrol", "particular/servicecontrol") .WithEnvironment("TransportType", "RabbitMQ.QuorumConventionalRouting") .WithEnvironment("ConnectionString", "host=host.docker.internal") .WithEnvironment("RavenDB_ConnectionString", "http://host.docker.internal:8080") .WithEnvironment("RemoteInstances", "[{\"api_uri\":\"http://host.docker.internal:44444/api\"}]") .WithEnvironment("PARTICULARSOFTWARE_LICENSE", license) .WithArgs("--setup-and-run") But the other two challenges are a bit harder to deal with. There is no built-in way in Aspire to "wait" for other resources to start. This isn't new to Aspire - in the past we had to write custom hooks in Docker Compose to wait for our dependencies' health checks to come back. The extensibility is there to do such a thing, so I found an extension to do just that.

The second problem was...a long slog to figure out. It's possible to have a Parallels VM be able to communicate with Docker containers running in the Mac host. However, I could not get this to work with Aspire. After doing side-by-side comparisons between container manifests running inside/outside of Aspire, I found the culprit:

"PortBindings": {"8080/tcp": [{-"HostIp": "",+"HostIp": "127.0.0.1","HostPort": "8000"}]}, With the Docker CLI, doing -p 8080:8000 does not set the host IP. Aspire does however, which means I can only access this container via localhost. Not ideal because my Windows VM is definitely not able to access that. Instead of using WithEndpoint or similar, I have to drop down to container runtime args:

.WithContainerRuntimeArgs("-p", "33333:33333").WaitFor(rabbitMqResource); Now my Service Control instance is up and running!

Setting up Service Control Audit, Monitoring, and Service PulseFollowing our previous example, we can finish out our configuration for the other container instances:

builder .AddContainer("servicecontrolaudit", "particular/servicecontrol-audit") .WithEnvironment("TransportType", "RabbitMQ.QuorumConventionalRouting") .WithEnvironment("ConnectionString", "host=host.docker.internal") .WithEnvironment("RavenDB_ConnectionString", "http://host.docker.internal:8080") .WithEnvironment("PARTICULARSOFTWARE_LICENSE", license) .WithArgs("--setup-and-run") .WithEndpoint(44444, 44444) .WaitFor(rabbitMqResource); builder .AddContainer("servicecontrolmonitoring", "particular/servicecontrol-monitoring") .WithEnvironment("TransportType", "RabbitMQ.QuorumConventionalRouting") .WithEnvironment("ConnectionString", "host=host.docker.internal") .WithEnvironment("PARTICULARSOFTWARE_LICENSE", license) .WithArgs("--setup-and-run") .WithEndpoint(33633, 33633) .WaitFor(rabbitMqResource); builder .AddContainer("servicepulse", "particular/servicepulse") .WithEnvironment("SERVICECONTROL_URL", "http://host.docker.internal:33333") .WithEnvironment("MONITORING_URL", "http://host.docker.internal:33633") .WithEnvironment("PARTICULARSOFTWARE_LICENSE", license) .WithEndpoint(9090, 9090) .WaitFor(rabbitMqResource); With all this in place in my Service Pulse instance is up and running:

And on the Service Insight side, I had to do the Parallels trick of using my hosts file to create a special "localhost.mac" entry to point to the Mac host:

10.211.55.2 localhost.mac With this in place, I can configure Service Insight in Windows to connect to the Docker Service Pulse instance running in Docker on the Mac:

All my NServiceBus messages and traces now show up just fine:

Most of the work I had to do was not really Aspire-related, but just configuring Aspire to pass in the appropriate configuration to the containers. You can find the full code to my configuration here:

Code Example

Enjoy!

View Details

Posts in this series:

  • Intro
  • Cataloging
  • Empty Proxy
  • Shared Library
  • Our First Controller
  • Migrating Initial Business Logic
  • Our First Views
  • Session State
  • Hangfire
  • Authentication
  • Middleware
  • Turning Off the Lights

In the last post, we looked at migrating our middleware, which we tackle in an as-needed basis. When a controller needs middleware to be migrated, we migrate that middleware over. If the entire app needs the middleware, it needs to come rather early.

Once we migrate much of our middleware over, it becomes much less work to incrementally migrate individual controllers and their subsequent actions/pages over. I won't go into deep detail into this part - mostly it's fixing namespaces, adjusting features (such as converting child actions into view components), but it can go quite fast. On recent teams I was working with, we migrated easily a dozen controllers a week amongst 3-4 developers. At this point, the bottleneck wasn't the conversion, but testing to make sure the pages still worked correctly

It's essentially testing the entire application, one page at a time, so hopefully you've got some regression tests in some form or fashion. I'm not skipping the incremental controller migration because it's not interesting - it's just because our teams really didn't encounter many challenges there. There will be something that comes up, there always is, but just the controller/action/view part is not too terrible.

But in this post I wanted to focus on getting to the end - what do we do once we've migrated everything but authentication? When there's just one controller left, we're now OK to proceed with migrating the last pieces over and "turning off the lights" on the .NET 4.x application.

Migrating Last FeaturesThe last (or next-to-last) migration typically:

  • Migrates the last controller, usually authentication
  • Turns off proxying and all remote app features

You don't necessarily need to split this into two separate units of work/deployments, as once you've migrated the last set of requests you can migrate all final features from the .NET Framework application. If the last controller is authentication, we'll also need to remove remote authentication. Our current web adapter configuration before final migration is:

builder.Services.AddSystemWebAdapters() .AddJsonSessionSerializer(options => { options.RegisterKey<string>("FavoriteInstructor"); }) .AddRemoteAppClient(options => { // Provide the URL for the remote app that has enabled session querying options.RemoteAppUrl = new(builder.Configuration["ProxyTo"]); // Provide a strong API key that will be used to authenticate the request on the remote app for querying the session options.ApiKey = builder.Configuration["RemoteAppApiKey"]; }) .AddAuthenticationClient(true) .AddSessionClient();builder.Services.AddHttpForwarder(); With middleware:

app.UseSystemWebAdapters();app.MapDefaultControllerRoute();app.MapForwarder("/{**catch-all}", app.Configuration["ProxyTo"]).Add( static builder => ((RouteEndpointBuilder)builder).Order = int.MaxValue); Along with migrating the authentication piece and all related middleware, we'll remove the above from our application startup, as well as the package references to all the proxy and System.WebAdapters packages. Once that's complete, our .NET application should now handle all requests. There might still be a few extra features to enable in .NET 8, such as Session:

builder.Services.AddSession();// laterapp.UseSession(); With all that complete, our .NET 8 application should now serve all requests and host all features needed to run our entire system.

Turning off the lightsWhile our .NET 8 application may now be "complete", we're not quite done yet. In my typical last phase we will:

  • Deploy the completed .NET 8 application to production
  • Monitor for any errors and any activity from the .NET 4.8 application
  • Adjust our .NET 8 application as necessary

If we don't see any issues, then the final final cleanup is:

  • Remove all .NET 4.8 code from the repository
  • Remove any shims to bridge from .NET 8 to .NET 4.8
  • Remove all .NET 4.8 application pipelines and deployments
  • Remove all .NET 4.8 production resources

And we should end with something like:

So what's next? There's still probably quite a bit to do to ".NET-8-ify" our existing system - all those architectural improvements we skipped in order to fast track migration. But most important - celebrate!

View Details

I've got another training event coming up focusing on Domain-Driven Design with Vertical Slice Architecture in Munich on October 21-23rd.

A little different than the previous times I've given this course is an option for either a 2-day or 3-day version. I had received feedback that folks were also interested in larger-scale design concepts such as bounded contexts, messaging, integration patterns, microservices, and modular monoliths. So I've included a 3rd day that covers these topics, where we look at encapsulation and cohesion at larger and larger scopes.

We'll cover:

  • Refactoring an existing system to leverage Vertical Slice Architecture
  • Applying Domain-Driven Design techniques to model complex business needs
  • Communication between slices
  • Exploring Validation and Testing (and other cross-cutting concerns) using Vertical Slice Architecture
  • Examining various design patterns, code smells, and refactoring techniques
  • Implementing the Vertical Slice Architectural pattern in various enterprise application scenarios (minimal APIs, Blazor, Web APIs, etc.)

And on the final day:

  • Service boundaries and bounded contexts
  • Communication between bounded contexts
  • Microservices and modular monoliths
  • Studying distributed systems patterns, tools, and libraries such as NServiceBus

The course pulls together my experiences building such systems for nearly 20 years now. And if you can't make the course during the day, I'm also hosting a networking event during the evening where you can meet myself and the other attendees and ask me questions. I hope to see you there!

Register Now

View Details

Posts in this series:

  • Intro
  • Cataloging
  • Empty Proxy
  • Shared Library
  • Our First Controller
  • Migrating Initial Business Logic
  • Our First Views
  • Session State
  • Hangfire
  • Authentication
  • Middleware

In the last post, we looked at tackling probably the most important pieces of middleware - authentication. But many ASP.NET MVC 5 applications will have lots of middleware, but not all of the middleware should be migrated without some analysis on whether or not that middleware is actually needed anymore.

This part is entirely dependent on your application - you might have little to no middleware, or lots. Middleware can also exist in a number of different places:

  • Web.config (you WILL forget this)
  • Global.asax (probably calling into other classes with the middleware configuration)
  • OWIN Startup

When choosing the first controller to migrate, I'm also looking at which controllers have the least amount of middleware, just to minimize the heavy first lift.

Let's look at our various middleware, and see what makes sense to move over, starting with our web.config.

Migrating Web.ConfigI think I forget the web.config middleware mainly because I've tried to burn most things ASP.NET from my brain. But we'll find lots of important hosting configuration settings in our web.config, from custom middleware to error handling, application configuration, server configuration and more. Luckily for us, nearly all out-of-the-box configuration has a direct analog in Kestrel. We mostly need to worry about anything custom here. My sample app doesn't have a lot going on:

<system.web> <compilation debug="true" targetFramework="4.8.1" /> <httpRuntime targetFramework="4.8.1" /> <customErrors mode="RemoteOnly" redirectMode="ResponseRewrite"> <error statusCode="404" redirect="/404Error.aspx" /> </customErrors> <!-- Glimpse: This can be commented in to add additional data to the Trace tab when using WebForms <trace writeToDiagnosticsTrace="true" enabled="true" pageOutput="false"/> --> <httpModules> <add name="Glimpse" type="Glimpse.AspNet.HttpModule, Glimpse.AspNet" /> </httpModules> <httpHandlers> <add path="glimpse.axd" verb="GET" type="Glimpse.AspNet.HttpHandler, Glimpse.AspNet" /> </httpHandlers> </system.web> We only have one set of custom modules/handlers and it's the now-dead (and much missed) Glimpse project. In the rest of the configuration, we only see custom errors redirecting to an .ASPX page, which we can easily port over using custom errors in ASP.NET Core. Otherwise there's not much going on here.

In a typical application, the things I've needed to migrate over might include such settings as:

  • Authentication
  • Authorization
  • Cookies
  • Session state
  • Data protection
  • Static files
  • HTTP request methods
  • Initialization
  • Custom headers
  • Caching

Each of these has some analog in ASP.NET Core Kestrel configuration. But luckily for us, we don't have any custom handlers/modules to worry about, only porting ASP.NET runtime features to ASP.NET Core.

ASP.NET MVC 5 MiddlewareNext up is ASP.NET MVC 5 middleware, which is typically set up in the Global.asax.cs file, something like:

AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); The global filters registered are:

public static void RegisterGlobalFilters(GlobalFilterCollection filters){ filters.Add(new HandleErrorAttribute()); filters.Add(new ValidatorActionFilter()); filters.Add(new MvcTransactionFilter()); } The first filter here is a built-in one from ASP.NET MVC to provide global error handling (with no extra configuration), but the second two are custom. The first custom filter provides some customization around handling validation errors and providing a common error result back to the UI:

public void OnActionExecuting(ActionExecutingContext filterContext) { if (!filterContext.Controller.ViewData.ModelState.IsValid) { if (filterContext.HttpContext.Request.HttpMethod == "GET") { var result = new HttpStatusCodeResult(HttpStatusCode.BadRequest); filterContext.Result = result; } else { var result = new ContentResult(); string content = JsonConvert.SerializeObject(filterContext.Controller.ViewData.ModelState, new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore }); result.Content = content; result.ContentType = "application/json"; filterContext.HttpContext.Response.StatusCode = 400; filterContext.Result = result; } } } The front end does still need this, so we want to port this over. The second filter provides automatic transaction handling:

public class MvcTransactionFilter : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { // Logger.Instance.Verbose("MvcTransactionFilter::OnActionExecuting"); var context = StructuremapMvc.ParentScope.CurrentNestedContainer.GetInstance<SchoolContext>(); context.BeginTransaction(); } public override void OnActionExecuted(ActionExecutedContext filterContext) { // Logger.Instance.Verbose("MvcTransactionFilter::OnActionExecuted"); var instance = StructuremapMvc.ParentScope.CurrentNestedContainer.GetInstance<SchoolContext>(); instance.CloseTransaction(filterContext.Exception); } } I might not do automatic transactions like this in a normal project but because the application code expects it, we'll need to migrate this over as well. The transaction filter is interesting because it highlights the shortcomings of ASP.NET MVC 5's dependency injection capabilities - namely there wasn't anything built in for filters. Instead of migrating this filter as-is, we need to translate to the equivalent ASP.NET Core filter:

public class DbContextTransactionFilter : IAsyncActionFilter { private readonly SchoolContext _dbContext; public DbContextTransactionFilter(SchoolContext dbContext) { _dbContext = dbContext; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { try { _dbContext.BeginTransaction(); var actionExecuted = await next(); if (actionExecuted.Exception != null && !actionExecuted.ExceptionHandled) { _dbContext.CloseTransaction(actionExecuted.Exception); } else { _dbContext.CloseTransaction(); } } catch (Exception ex) { _dbContext.CloseTransaction(ex); throw; } } } And we register our filter:

builder.Services.AddControllersWithViews(opt =>{ opt.Filters.Add<DbContextTransactionFilter>();}); Now our filter will have its DbContext injected instead of going out to a custom extension to mimic per-request service lifetimes.

Finally, let's look at the OWIN middleware.

OWIN MiddlewareOWIN middleware can be found in classes with the OwinStartup attribute configured for them. Usually this is a "Startup" class but it could be anything. In my sample app, we have:

[assembly: OwinStartup(typeof(Startup))]namespace ContosoUniversity{ public partial class Startup { public void Configuration(IAppBuilder app) { app.MapSignalR(); GlobalConfiguration.Configuration .UseSqlServerStorage("SchoolContext") .UseStructureMapActivator(IoC.Container) ; app.UseHangfireDashboard(); app.UseHangfireServer(new BackgroundJobServerOptions { Queues = new[] { Queues.Default } }); ConfigureAuth(app); } }} Basically, it's:

  • SignalR
  • Hangfire
  • Authentication

Authentication might differ slightly than the ASP.NET authentication, so we'll want to port settings there. SignalR and Hangfire can be dealt with individually, but otherwise we don't have any custom OWIN middleware. This is fairly typical unless your application wholly relies on OWIN instead of say, IIS.

Middleware isn't the most exciting code to port over, but critical for ensuring our new application preserves the existing behavior of the .NET Framework application.

In our last post, we'll cover finishing up our migration and "turning off the lights".

View Details

The last training course in Zurich was a success, in that no laptops were harmed. I think. I put a poll out on where I should do the training next and quite a few folks suggested the Netherlands. I'm happy to announce that the next VSA course will be in the Netherlands on July 17-18th.

This course approaches this topic from the perspective of refactoring an existing system to this architecture. We also look at larger and larger boundaries of cohesion, from applications to services to systems. I'm also doing something new, a Q&A at a pub where you can ask questions while we share authentic Dutch beer (Heineken).

More details here:

Vertical Slice Architecture Training

Hope to see you there!

View Details

Posts in this series:

  • Intro
  • Cataloging
  • Empty Proxy
  • Shared Library
  • Our First Controller
  • Migrating Initial Business Logic
  • Our First Views
  • Session State
  • Hangfire
  • Authentication

Of all the topics in .NET migration, authentication, like always, is the one that is most characterized by "It Depends". The solution for addressing authentication is wholly dependent on what the current authentication solution is in the current .NET 4.8 application. If you're doing external SSO, then it's likely quite simple - the new solution is simply a new client for your external SSO.

In my situation, the .NET Framework application was responsible for authentication, i.e., it had a login screen. It was a home-grown identity provider, not using ASP.NET Identity. If you're using ASP.NET Identity and all the database backing stores, you're also looking at a data migration. I'll leave that as an exercise to the reader ;)

The end result we're looking for is:

  • Users can log in via one of the apps (.NET 8 or .NET 4.8)
  • Once logged in, both apps recognize the user as authenticated and can read identical claims/roles
  • Users can log out via one of the apps

Our two dumbed down options available to solving this are:

  • Remote authentication in ASP.NET 4.8
  • Cookie sharing between ASP.NET 4.8 and ASP.NET Core

The cookie sharing option is intriguing but it has some limitations:

  • Only works with Microsoft.Owin cookie authentication
  • Requires shared cookie and data protection configuration between applications

Our application didn't have that first constraint so we couldn't consider it. Remote authentication works by:

  • Users log in and out of the ASP.NET 4.8 application
  • ASP.NET Core adapters call APIs in ASP.NET 4.8 to retrieve user authentication information (claims) and populates its claims identity with this data

It's very similar to the remote session story:

Except getting we're getting the claims information from the ASP.NET application. This means, however, that the login/logout endpoints will need to be migrated last. Which means if our authentication story is complicated, we'll have plenty of runway since it'll be last.

Configuring Remote AuthenticationConfiguring remote authentication is straightforward if we've already added the remote app server for session. We add a single line of code to the ASP.NET application to AddAuthenticationServer:

this.AddSystemWebAdapters() .AddJsonSessionSerializer(options => { options.RegisterKey<string>("FavoriteInstructor"); }) // Provide a strong API key that will be used to authenticate the request on the remote app for querying the session // ApiKey is a string representing a GUID .AddRemoteAppServer(options => options.ApiKey = ConfigurationManager.AppSettings["RemoteAppApiKey"]) .AddAuthenticationServer() .AddSessionServer(); And in our ASP.NET Core application, to add the authentication client:

builder.Services.AddSystemWebAdapters() .AddJsonSessionSerializer(options => { options.RegisterKey<string>("FavoriteInstructor"); }) .AddRemoteAppClient(options => { // Provide the URL for the remote app that has enabled session querying options.RemoteAppUrl = new(builder.Configuration["ProxyTo"]); // Provide a strong API key that will be used to authenticate the request on the remote app for querying the session options.ApiKey = builder.Configuration["RemoteAppApiKey"]; }) .AddAuthenticationClient(true) .AddSessionClient(); There's a ton of options, because of course authentication is complicated, but this also means we can turn on authentication and authorization as normal in our ASP.NET Core application:

app.UseRouting();app.UseAuthentication();app.UseAuthorization();app.UseSystemWebAdapters(); With this in place, we can access all the normal ClaimsPrincipal and IIdentity details anywhere inside our ASP.NET Core application. We can't examine the security cookie - but we shouldn't anyway, our application code should only be concerned with the principal and identity, not the underlying details of how that got populated.

If we need to add more claims, those will get added on the ASP.NET side and automatically populated on the ASP.NET Core side with those API calls back to get all the claims for the user. It's another clever shim to allow us to migrate all controllers, actions, and application code that require authentication and authorization.

In the next post, we'll look at the middleware that exists in the ASP.NET application and migrate anything we actually want to migrate, and leave the rest behind.

View Details

Something new I'm starting this year is a two-day course on Modern .NET systems with Vertical Slice Architecture. It contains a lot of topics that I've consulted with organizations and built systems around for around over a decade now, and I wanted to wrap my learnings up into a single training course.

And since most of my systems I deal with are not greenfield, but dealing with existing systems, this course focuses on refactoring a system using Vertical Slice Architecture, and all the patterns, tools, and libraries that come along with it. In particular, I'll be focusing on:

  • Refactoring an existing system to leverage Vertical Slice Architecture
  • Applying Domain-Driven Design techniques to model complex business needs
  • Exploring various design patterns, code smells, and refactoring techniques
  • Using the Vertical Slice Architectural pattern in a variety of modern .NET 8 application scenarios (minimal APIs, Blazor, Web APIs, etc.)
  • Effective use of common libraries such as AutoMapper and MediatR
  • Examining distributed systems patterns, tools, and libraries such as NServiceBus

The training will be in Zurich, Switzerland on April 9-10. Use the early bird voucher code EarlyBird20 through the end of February for a 20% discount:

Register Now

I hope to see you there!

View Details

Today I pushed out AutoMapper 13.0 (is that too many...?):

  • Release Notes
  • Changelog
  • NuGet
  • Upgrade Guide

Probably the biggest change with this release is folding in Microsoft.Extensions.DependencyInjection support directly. The AutoMapper.Extensions.Microsoft.DependencyInjection package is deprecated as a result.

Side note, the docs were messed up with this version so go to the "latest" version to see them.

View Details

Posts in this series:

  • Intro
  • Cataloging
  • Empty Proxy
  • Shared Library
  • Our First Controller
  • Migrating Initial Business Logic
  • Our First Views
  • Session State
  • Hangfire
  • Authentication

In the last post, we encountered our first instance of shared runtime data between our different ASP.NET 4.8 and ASP.NET Core applications, in Session State. There are other mechanisms to store state in ASP.NET 4.8 (such as Application state), but Session is the most common. In this post, we'll look at another instance of shared state that isn't built in to ASP.NET, but I find quite common - Hangfire.

Hangfire is an easy way to perform background tasks/processes in a .NET web application, and it also supports persistent storage for both the jobs and queues. I use it quite a lot in applications where I don't want to introduce a separate host for processing messages, or introduce a specific queue/broker for background jobs. Hangfire supports fire-and-forget jobs as well as "cron"-based jobs. It also provides a nice dashboard where you can see completed and failed jobs, with the option of retrying failed jobs as desired.

Depending on how you're using Hangfire, it introduces a unique challenge when migrating from .NET 4.8 to .NET 6/7/8. Hangfire supports both frameworks, but as usual, the devil is in the details. We want to be able to start/consume jobs from both sides AND ensure our job executes at most once.

First, let's look to see how we configure and use Hangfire today.

ASP.NET 4.8 Hangfire UsageIn our OWIN startup in the ASP.NET 4.8 application, we find our Hangfire configuration:

GlobalConfiguration.Configuration .UseSqlServerStorage("SchoolContext") .UseStructureMapActivator(IoC.Container) ;app.UseHangfireDashboard();app.UseHangfireServer(); We can see here that we're using SQL Server for our storage (jobs and queues), and that we're using a DI container (StructureMap) for activating/instantiating jobs. We don't see it explicitly configured but our job is using the default queue, named default.

Our jobs can be enqueued anywhere really, from controllers to services to startup. For anything that migrates to ASP.NET Core that uses Hangfire, we'll have to migrate that usage as well. Here's one usage:

[HttpPost][ValidateAntiForgeryToken]public async Task<ActionResult> Edit(Edit.Command command){ await _mediator.Send(command); _backgroundJobClient.Enqueue(() => LogEdit(command)); return this.RedirectToActionJson(c => c.Index(null));}[NonAction]public void LogEdit(Edit.Command command){ _logger.Information($"Editing student {command.ID}");} It's completely trivial, but let's assume the background job is actually doing something interesting, like sending emails or SMS messages.

If we were to migrate this by itself over to ASP.NET Core, we'll immediately run into an issue - Hangfire is now running in two places - ASP.NET and ASP.NET Core, and if we do nothing additional, Hangfire in each server will try to consume those jobs. Unfortunately, it might not be able to execute those jobs. In the above example, the job exists simply as a method from my controller - a perfectly valid way of using Hangfire. If this method only exists in one of the web applications, the other web application won't be able to execute the job and it will wind up failing.

Hangfire does support web farm scenarios and the competing consumer pattern, so we still know that only one side or the other will pick up the job. But it might not be able to execute it if the job code isn't there.

We could fix this by migrating all of our job code to the "shared" assembly first, but this might be a complex undertaking especially if we're using the pattern above. Instead, we can create separate queues for each host - ASP.NET 4.8 and ASP.NET Core, and ensure the job is queued to the host where that job code lives.

When both live in ASP.NET Core:

\When the initiator is ASP.NET Core and the job lives in ASP.NET 4.8:

And the reverse:

And finally solely ASP.NET 4.8:

With this setup, the job initiator must "know" where the job code lives. This might seem like unnecessary coupling, but keep in mind this is transitional configuration and we won't need to have this knowledge baked in once all of the jobs and initiators are migrated.

Configuring for Multiple HostsInitially, we did not specify any queue in our Hangfire configuration. Now, we'll be explicit in ASP.NET 4.8:

app.UseHangfireServer(new BackgroundJobServerOptions{ Queues = new[] { Queues.Default }}); And after pulling in the appropriate packages to ASP.NET Core, we configure our startup there with the other queue:

builder.Services.AddHangfire(cfg =>{ cfg.UseSqlServerStorage( builder.Configuration.GetConnectionString("SchoolContext"));});builder.Services.AddHangfireServer(options => options.Queues = new[] { Queues.DefaultCore }); I could migrate the Hangfire dashboard over to ASP.NET Core, but I left it alone for now. The YARP piece will take care of that for now.

For jobs that start and stay inside of one host, there's nothing we need to do to specify a queue. However, for jobs that cross host boundaries, we'll specify the queue name in the job:

[NonAction][Queue(Queues.DefaultCore)]public void LogEdit(Edit.Command command){ _logger.Information($"Editing student {command.ID}");} This ensures that the jobs start and stop where they're supposed to, and are only executed at most once. Once all of our jobs are migrated, we can rename the queue to the default name (as long as we've drained our job queues beforehand).

So far, all of our actions don't require a logged in user. In the next post, we'll tackle authentication.

View Details

Posts in this series:

  • Intro
  • Cataloging
  • Empty Proxy
  • Shared Library
  • Our First Controller
  • Migrating Initial Business Logic
  • Our First Views
  • Session State
  • Hangfire
  • Authentication

Believe it or not, things have been relatively simple so far. In the next few posts, we'll get to the more interesting/complicated bits. First up is session state. If your app doesn't use session state - congratulations! You're one of the lucky few. But most reasonably sized applications I see these days make some use of session state, as a way to provide a per-user cache of data.

The good news is that ASP.NET Core supports, and has supported, session state for a very long time. This means it's a robust feature that you can rely on working solidly for you in your new application. The bad news is there is zero backwards compatibility between ASP.NET Core and ASP.NET. This means we have a couple options to us:

  • Migrate away from ASP.NET Session state or create something custom...somehow
  • Get the two applications to play nice...somehow

The first option might be a possibility if your application doesn't use Session that much or can do without, at least until post-migration. But for most systems I run into, Session was added for Very Good reasons and it's not easily taken away.

While ASP.NET Core isn't backwards compatible, luckily for us option 2 is available to us with the Microsoft.AspNetCore.SystemWebAdapters library.

Incremental Session State Migration OptionsWhile ASP.NET Core has session state, it's not directly backwards compatible for a number of reasons, first and foremost that the implementation is completely different. The spirit of session state was carried forward, but none of the "bad stuff". ASP.NET Session did a few things "automatically" for you, including locking and serialization. With ASP.NET Core, there's no locking and no default serialization. You're given a key and byte[], but there are libraries and extensions to make serialization easier.

The adapters library gives us two options to migrate session state:

  • Remote app
  • Wrapped ASP.NET Core

Remote app is quite clever and we'll see this technique used in the future. The ASP.NET application exposes API endpoints for session state. The ASP.NET Core application can then call these API endpoints to get/set session state:

With this approach, the ASP.NET Core application doesn't care how the ASP.NET application stores its session state. In a web farm scenario, it's common to store session state in SQL Server. ASP.NET Core also supports this storage, but of course it's a completely different schema/format etc.

The other option is a "wrapped" session state in ASP.NET Core, which makes session available to the System.Web adapters (and therefore easier to migrate at the end).

Configuring Remote Session StateWith remote session state, we first need to expose our API endpoints in the ASP.NET application:

SystemWebAdapterConfiguration.AddSystemWebAdapters(this) // Provide a strong API key that will be used to authenticate the request on the remote app for querying the session // ApiKey is a string representing a GUID .AddRemoteAppServer(options => options.ApiKey = ConfigurationManager.AppSettings["RemoteAppApiKey"]) .AddSessionServer(); The "RemoteAppApiKey" is a string (typically a GUID) that represents a shared secret between the ASP.NET Core client and ASP.NET server. When the ASP.NET Core client calls any APIs (such as session), this API is included as a header to authenticate those calls.

Next we need to find out what usages of Session we have in ASP.NET to share over with ASP.NET Core, because each of those keys and serialization mechanisms need to be registered. Looking in my sample application, I just have the one:

[HttpPost][ValidateAntiForgeryToken]public async Task<ActionResult> Edit(CreateEdit.Command command){ await _mediator.Send(command); Session["FavoriteInstructor"] = $"{command.FirstMidName} {command.LastName}"; return this.RedirectToActionJson("Index");} Yes it's silly but I'm also lazy. The key part here is understanding what keys and values are being used. Old in-memory session state from ASP.NET was...let's say generous? About what was stored. In the new world, we have to explicitly serialize. So why not JSON? Back in our ASP.NET Global.asax, let's register this key and serializer:

this.AddSystemWebAdapters() .AddJsonSessionSerializer(options => { options.RegisterKey<string>("FavoriteInstructor"); }) .AddRemoteAppServer(options => options.ApiKey = ConfigurationManager.AppSettings["RemoteAppApiKey"]) .AddSessionServer(); Your application may require changes in the objects you keep in session to be able to expose them via an API.

Next, we'll need to register our adapters and client on the ASP.NET Core side:

builder.Services.AddSystemWebAdapters() .AddJsonSessionSerializer(options => { options.RegisterKey<string>("FavoriteInstructor"); }) .AddRemoteAppClient(options => { options.RemoteAppUrl = new(builder.Configuration["ProxyTo"]); options.ApiKey = builder.Configuration["RemoteAppApiKey"]; }) .AddSessionClient(); Remember, all keys and objects must be explicitly registered. This may seem like a hassle, but our ASP.NET Core server must include code changes to read/write from these remote session items so it's not that much of a problem in practice.

Finally, we need to make our remote session available to inject into our controllers:

app.MapDefaultControllerRoute() .RequireSystemWebAdapterSession(); To use the session state, the reads and writes are as normal on our .NET Framework ASP.NET application:

[HttpPost][ValidateAntiForgeryToken]public async Task<ActionResult> Edit(CreateEdit.Command command){ await _mediator.Send(command); Session["FavoriteInstructor"] = $"{command.FirstMidName} {command.LastName}"; return this.RedirectToActionJson("Index");} But on the ASP.NET Core side, we need to go through the System.Web adapters:

public ActionResult Index(){ ViewBag.FavoriteInstructor = System.Web.HttpContext.Current?.Session?["FavoriteInstructor"] ?? string.Empty; return View();} Technically we can inject the ISessionManager object into our controller but its definition is a little wonky to use in practice. So if you need to unit test/mock Session, you'd want that interface.

When we include the Session on every request, then every request will call back to the ASP.NET application to get its entire session and store it locally (and manage updates). When using the Session attribute on controllers, this session is only requested for that single controller. Updates are managed via middleware, POSTing the session data from the ASP.NET Core application back to the ASP.NET application.

At some point, we will need to migrate all of the session state over to the ASP.NET Core application, but we'll cover this at the end with the "turning off the lights" step.

In the next post, we'll look at another common "shared state" problem - background tasks with Hangfire.

View Details

Posts in this series:

  • Intro
  • Cataloging
  • Empty Proxy
  • Shared Library
  • Our First Controller
  • Migrating Initial Business Logic
  • Our First Views
  • Session State
  • Hangfire
  • Authentication

Back when we looked at our first controller, we tried out the "automatic" migration and the controllers migrated just fine but our views did not:

<head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title - My ASP.NET Application</title> @*@Styles.Render("~/Content/css")*@ @*@Scripts.Render("~/bundles/modernizr")*@</head><body> <!-- junk --> @*@Scripts.Render("~/bundles/jquery")*@ @*@Scripts.Render("~/bundles/bootstrap")*@ @RenderSection("scripts", required: false)</body> Those commented out sections are there because ASP.NET Core does not support bundling and minification. We'll need to figure out an alternative solution for that. We also need to check any other components or libraries to see if there are compatible versions for ASP.NET Core. Front-end assets don't really need "migrating" so those should work just fine.

But if there are components that plug in to ASP.NET 4.8 itself, we'll need to address those as well. In our real-world application, we had two problematic components:

  • A "file upload" widget that included an .ASHX handler for uploads
  • The HtmlTag library that exposes custom HtmlHelper extensions

The file upload widget had no upgrade path - it was a legacy component that hadn't had a new release in many years, let alone any version targeting ASP.NET Core.

For the HtmlTag situation, that library is maintained and supports ASP.NET Core directly. We'll need to port our existing usage to the latest version of the library. This really just leaves the bundling and minification.

Modernizing Bundling and MinificationBecause there is no out-of-the-box solution for bundling and minification in ASP.NET Core, we need to decide what our solution should be in our new ASP.NET Core world. Luckily, the ASP.NET Core docs on this topic give us some insight on our two basic options:

  • An OSS library that's very similar to the capabilities in ASP.NET MVC 5 (WebOptimizer)
  • 3rd-party libraries that aren't tied to ASP.NET Core (Webpack, etc.)

A 3rd-party library might be an option if we're also looking at incorporating more modern JavaScript libraries in the future. From a strict "minimal lift-and-shift" perspective, WebOptimizer looks better.

Integrating WebOptimizer is pretty simple, we first add the package:

<PackageReference Include="LigerShark.WebOptimizer.Core" Version="3.0.384" /> And configure it in our application startup:

builder.Services.AddWebOptimizer(pipeline => BundleConfig.BundlingPipeline(pipeline, builder.Environment.IsDevelopment()) ); The existing bundle configuration from the ASP.NET MVC 5 app looks like:

public class BundleConfig{ public static void RegisterBundles(BundleCollection bundles) { bundles.Add(new ScriptBundle("~/bundles/jquery").Include( "~/Scripts/jquery-{version}.js")); // other JS bundles.Add(new StyleBundle("~/Content/css").Include( "~/Content/bootstrap.css", "~/Content/site.css")); BundleTable.EnableOptimizations = true; }} We found that although the bundling capabilities of MVC 5 and ASP.NET Core were similar, they were not identical. Additionally, we needed to decide if we were going to migrate all of our front-end assets at the same time. Many pages included their own assets in separate bundles, and the URLs for the final bundles were slightly different.

We preferred to migrate incrementally as much as possible and not have any big bang migrations. If we tried to migrate all of our bundling all at once, we'd have to figure out how to deal with all of the existing pages that have bundles that don't work - since typically bundles are defined on shared layouts.

To make sure that we can support both applications each with their own bundling schemes, we decided that each app will do its own bundling, but the static assets will only live in one place in the repository. We migrated the configuration of the bundling to WebOptimizer (abridged version):

private static void ProcessScripts(IAssetPipeline pipeline){ pipeline.AddJavaScriptBundle("/Scripts/bundles/jquery.js", "/wwwroot/Scripts/jquery-2.1.4.js") .UseFileProvider(_fileProvider); pipeline.AddJavaScriptBundle("/Scripts/bundles/jqueryval.js", "/wwwroot/Scripts/jquery.validate.*") .UseFileProvider(_fileProvider); pipeline.AddJavaScriptBundle("/Scripts/bundles/modernizr.js", "/wwwroot/Scripts/modernizr-*") .UseFileProvider(_fileProvider); pipeline.AddJavaScriptBundle("/Scripts/bundles/bootstrap.js", "/wwwroot/Scripts/bootstrap.js", "/wwwroot/Scripts/respond.js") .UseFileProvider(_fileProvider); pipeline.AddJavaScriptBundle("/Scripts/bundles/lodash.js", "/wwwroot/Scripts/lodash.js") .UseFileProvider(_fileProvider);} Then our ASP.NET Core layout can use this new way of doing bundles:

<head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title - Contoso University</title> <link rel="stylesheet" href="/Content/css.css" /> <script src="/Scripts/bundles/modernizr.js"></script> <script src="/Scripts/bundles/lodash.js"></script> It's not identical to what we saw before (there are many options we can configure with WebOptimizer omitted here), and we found it impossible to exactly emulate the bundling of ASP.NET MVC 5. We just needed to make sure we didn't duplicate files by altering our ASP.NET Core application to pull in the ASP.NET MVC 5 assets through linking:

<Content Include="..\ContosoUniversity\Content\**\*.*" Link="wwwroot\Content\%(RecursiveDir)%(Filename)%(Extension)"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory></Content><Content Include="..\ContosoUniversity\Scripts\**\*.*" Link="wwwroot\Scripts\%(RecursiveDir)%(Filename)%(Extension)"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory></Content> These files are in the old project, but linked into the new project - a technique I've use many times in the past:

If we add a new asset file to the ASP.NET MVC 5 project, it will automatically show up here as well.

This is only a temporary shim, as once we've moved all of the controllers/actions over, we can also move all of the static assets over and remove the "linking" altogether. Our ASP.NET Core application doesn't care however, it will transparently use the files in the new location. Although it added some extra steps in the interim, we preferred this approach since it eliminated any big-bang changes.

As we migrate controllers, we'll encounter features and middleware not yet migrated. In the next post, we'll look at our first major middleware feature to share across applications - Session.

View Details

Posts in this series:

  • Intro
  • Cataloging
  • Empty Proxy
  • Shared Library
  • Our First Controller
  • Migrating Initial Business Logic
  • Our First Views
  • Session State
  • Hangfire
  • Authentication

In the last post we moved just our initial controller over but none of the code used by the controller yet. The first "feature" migrated will be this controller and its vertical slice of backend and frontend functionality. We picked a "small" and easy controller but in practice we found that this first controller inevitably brings quite a bit of other baggage along with it, like:

  • DI container configuration
  • ORM setup and configuration
  • Middleware
  • Layouts, view components, and HTML helpers
  • Static assets (JS, CSS)

It's not really possible to skip these for our first shipped feature, so our team tried to break down each of these separately:

  • Request pipeline junk (Controller, filters, middleware)
  • Business logic junk
  • HTML junk

Basically each part of the "MVC" pattern separately. Right now, we have a migrated controller but none of the associated middleware. This is because our first controller didn't require any of our customized middleware but this will change soon.

Next up is the "M" part of MVC which really just entails "everything needed to produce a View Model". It can be tempting to try and port all business logic all at once but that can skip some very important steps, such as evaluating our dependencies and determining if we need to upgrade first before migration. For example, if your existing application logic uses some custom logger or 3rd party logger, you may want to migrate to Microsoft's ILogger<T> first before moving your business logic. Regardless, we need to examine our controller's logic and vertical slices of functionality to understand what needs to be migrated over.

Analyzing Existing DependenciesFirst, our controller's constructor:

public HomeController(IMediator mediator){ _mediator = mediator;} Right away we can see that our ASP.NET MVC 5 is configured to use dependency injection, and its injection a MediatR IMediator interface. We need to decide:

  • What DI container to use in the .NET 6 application
  • How to register MediatR with the container

DI container registration in ASP.NET MVC 5 was...ugly to say the least. Quite a bit of extra middleware and setup existed solely to work around the lack of a built-in container. Nested containers, custom startup methods, custom filters, it was a mess. I'm going to ignore all of that except to call out that your application may have a bit of static service location going on. You'll again need to decide if you want to modernize or migrate.

Looking at the container's configuration/registration code, it's pretty simple:

public static class IoC { private static IContainer _container; public static IContainer Container => _container ?? (_container = Initialize()); private static IContainer Initialize() { return new Container( c => c.AddRegistry<DefaultRegistry>()); }} It's a single entry point to a registration class that has all the gory details:

public class DefaultRegistry : Registry { #region Constructors and Destructors public DefaultRegistry() { Scan( scan => { scan.TheCallingAssembly(); scan.WithDefaultConventions(); scan.LookForRegistries(); scan.AssemblyContainingType<DefaultRegistry>(); scan.AssemblyContainingType<ILogger>(); scan.AddAllTypesOf(typeof(IModelBinder)); scan.AddAllTypesOf(typeof(IModelBinderProvider)); scan.With(new ControllerConvention()); }); For<SchoolContext>().Use(() => new SchoolContext("SchoolContext")) .LifecycleIs<TransientLifecycle>(); For<IControllerFactory>().Use<ControllerFactory>(); For<ModelValidatorProvider>().Use<FluentValidationModelValidatorProvider>(); For<IValidatorFactory>().Use<StructureMapValidatorFactory>(); For<IBackgroundJobClient>().Use<BackgroundJobClient>(); For<IBackgroundJobFactory>().Use<BackgroundJobFactory>(); For<IBackgroundJobStateChanger>().Use<BackgroundJobStateChanger>(); var connectionString = ConfigurationManager.ConnectionStrings["SchoolContext"].ConnectionString; For<JobStorage>().Use(new SqlServerStorage(connectionString)); For<IJobFilterProvider>().Use(JobFilterProviders.Providers); For<IServiceProvider>().Use(ctxt => new StructureMapServiceProvider(ctxt)); For<INotificationPublisher>().Use<ForeachAwaitPublisher>(); } #endregion} Well, there's a LOT going on here. This registrations class:

  • Scans assemblies and registers:
    • Classes with default conventions (IFoo to Foo)
    • Looks for additional registries
    • Adds all implementations of IModelBinder
    • Adds all implementations of IModelBinderProvider
    • Adds all controllers
  • Registers explicitly:
    • EF6
    • Fluent Validation and integration with MVC 5
    • Hangfire services and persistence
    • Individual application-specific services

We also have other registration classes:

  • AutoMapperInitializer.cs (self-explanatory)
  • CommandProcessingRegistry.cs - registers both MediatR and FluentValidation
  • HtmlTagRegistry.cs - registers HtmlTags

The stock container would work with the ".NET Core" version of all of these 3rd-party dependencies. Each either has built-in support or an extension library for the Microsoft.Extensions.DependencyInjection stock DI container.

What's not in the stock DI container is any support for scanning and auto-registration. In this application there's not too much of that but in larger applications there might be a lot of features that leverage custom registration capabilities, resolution, or other random features not present in the stock container. Because of this, I'll opt for modernization and move my container usage from StructureMap to Lamar, the rewrite of StructureMap for .NET Core. As a bonus, a lot of my registration code will work "as-is", depending on if I want to keep it or not.

Finally, the business logic itself:

public class About{ public class EnrollmentDateGroup { [DataType(DataType.Date)] public DateTime? EnrollmentDate { get; set; } public int StudentCount { get; set; } } public class Query : IRequest<IEnumerable<EnrollmentDateGroup>> { } public class Handler : IRequestHandler<Query, IEnumerable<EnrollmentDateGroup>> { private readonly SchoolContext _dbContext; public Handler(SchoolContext dbContext) { _dbContext = dbContext; } public async Task<IEnumerable<EnrollmentDateGroup>> Handle(Query message, CancellationToken token) { // Commenting out LINQ to show how to do the same thing in SQL. //IQueryable<EnrollmentDateGroup> = from student in db.Students // group student by student.EnrollmentDate into dateGroup // select new EnrollmentDateGroup() // { // EnrollmentDate = dateGroup.Key, // StudentCount = dateGroup.Count() // }; // SQL version of the above LINQ code. string query = "SELECT EnrollmentDate, COUNT(*) AS StudentCount " + "FROM Person " + "WHERE Discriminator = 'Student' " + "GROUP BY EnrollmentDate"; IEnumerable<EnrollmentDateGroup> data = await _dbContext.Database .SqlQuery<EnrollmentDateGroup>(query) .ToListAsync(); return data; } }} Yes this "business logic" is a bit weird but it's what existed in the original ContosoUniversity EF6/MVC 5 sample app that I converted to vertical slice architecture. What's important here is that it only depends on the EF6 DbContext so if I migrate that I should be OK. And in the "Shared Library" post, we already migrated it to a netstandard2.1-targeted library, so we're all set.

Migrating DependenciesKeeping with the theme of "Only Migrate What's Required", I only want to bring in Lamar, MediatR, and EF6. This will also force me to migrate a little bit of configuration (the connection string). After pulling in the Lamar package, the initialization simply becomes:

builder.Host.UseLamar(registry => registry.IncludeRegistry<DefaultRegistry>()); With the DefaultRegistry migrating to:

public class DefaultRegistry : ServiceRegistry{ public DefaultRegistry() { Scan( scan => { scan.TheCallingAssembly(); scan.WithDefaultConventions(); scan.LookForRegistries(); scan.AssemblyContainingType<DefaultRegistry>(); scan.AssemblyContainingType<ILogger>(); scan.AddAllTypesOf(typeof(IModelBinder)); scan.AddAllTypesOf(typeof(IModelBinderProvider)); }); For<SchoolContext>() .Use(ctxt => new SchoolContext(ctxt.GetInstance<IConfiguration>().GetConnectionString("SchoolContext")))) .Scoped(); For<INotificationPublisher>().Use<ForeachAwaitPublisher>(); }} Some things I've left out because they're not migrated, but others simply aren't needed anymore. Technically the IModelBinder and IModelBinderProvider custom implementations haven't been migrated but I pulled those two lines over and simply corrected the namespace.

The connection string I'll pull over from the original Web.config and drop it into the appsettings.Development.json file:

"ConnectionStrings": { "SchoolContext": "Data Source=.\\sqlexpress;Initial Catalog=ContosoUniversity;Integrated Security=SSPI;"} For MediatR, I can now use the built-in registration extension:

builder.Services.AddMediatR(config =>{ config.RegisterServicesFromAssemblyContaining<HomeController>();}); The business logic is copy-and-paste with no changes. We already migrated common dependencies and logic into a shared library so our .NET 6 and .NET 4.8 app can both reference and use the same EF6 code and models.

With this in place, the business logic all compiles and runs, riiiiight until we need to show a view. In the next post, we'll climb the next hill in our journey, migrating our first views.

View Details

Posts in this series:

  • Intro
  • Cataloging
  • Empty Proxy
  • Shared Library
  • Our First Controller
  • Migrating Initial Business Logic
  • Our First Views
  • Session State
  • Hangfire
  • Authentication

In the last post, we prepped for our first set of pages migrated by extracting common logic into a shared library. With that in place, we're now ready to migrate our first controller. I like to do a single controller at a time rather than individual actions because controllers are a natural grouping of common behavior that often have interdependencies amongst actions, may share components, may share logic, etc.

Our first controller needs to be one that has enough representative front-end dependencies - but not too many actions going on. Ideally, we can migrate something with minimum business logic. That was one of the goals of cataloging our application - to help figure out where we should start. From that analysis, we can see that the "HomeController" only has one dependency and most of the actions do basically nothing but show a view with no real work being done.

One of the things that the latest drop of the .NET Upgrade Assistant is that it includes a controller migration action. We can right-click a controller in the MVC 5 application and there's an "Upgrade Controller" option:

Upgrade Controller menu optionIf we run this on a "stock" demo application, it can give us an idea of what this option actually does. Here's the original controller:

using System.Web.Mvc;namespace UpgradeSample.Controllers{ public class HomeController : Controller { public ActionResult Index() { return View(); } public ActionResult About() { ViewBag.Message = "Your application description page."; return View(); } public ActionResult Contact() { ViewBag.Message = "Your contact page."; return View(); } }} And after the upgrade, our new controller:

using Microsoft.AspNetCore.Mvc;namespace UpgradeSample.Controllers{ public class HomeController : Controller { public ActionResult Index() { return View(); } public ActionResult About() { ViewBag.Message = "Your application description page."; return View(); } public ActionResult Contact() { ViewBag.Message = "Your contact page."; return View(); } }} Looks nearly identical, just a using change. In practice, I found much of the same. Most of the time it's namespace changes, with other small changes (HtmlString to IHtmlString, little things like that). None of these are particularly difficult to work with.

When we go look at the migrated views, that's when we start to see some...issues. Here's the head section of our original layout:

<head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title - My ASP.NET Application</title> @Styles.Render("~/Content/css") @Scripts.Render("~/bundles/modernizr")</head> We're using the built-in bundler and minifier of ASP.NET MVC 5. Here's the ASP.NET Core version:

<head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title - My ASP.NET Application</title> @*@Styles.Render("~/Content/css")*@ @*@Scripts.Render("~/bundles/modernizr")*@</head><body> <!-- junk --> @*@Scripts.Render("~/bundles/jquery")*@ @*@Scripts.Render("~/bundles/bootstrap")*@ @RenderSection("scripts", required: false)</body> Now we're hit with our first big problem - ASP.NET Core does not have any built-in bundling and minification. As a feature, it just does not and will not ever exist. There's not any docs in the migration sections but we can check to see how ASP.NET Core recommends to do bundling and minification. Your situation might be different, but we wanted minimum changes to our apps so we decided to go the WebOptimizer route.

The HtmlHelper extensions mostly work, but if you have custom extensions those are manual migrations.

Dealing with Feature FoldersOne other big difference in our sample application is that it uses feature folders with a custom view engine. All of the views and code are placed together:

Feature foldersNow comes our first difficult decision - do we keep this structure in the new application? Or try to maintain it? I do like feature folders BUT if it makes migration harder I'm OK ditching it (for now). The existing custom view engine isn't too complicated:

public class FeatureViewLocationRazorViewEngine : RazorViewEngine{ public FeatureViewLocationRazorViewEngine() { ViewLocationFormats = new[] { "~/Features/{1}/{0}.cshtml", "~/Features/{1}/{0}.vbhtml", "~/Features/Shared/{0}.cshtml", "~/Features/Shared/{0}.vbhtml", }; MasterLocationFormats = ViewLocationFormats; PartialViewLocationFormats = new[] { "~/Features/{1}/{0}.cshtml", "~/Features/{1}/{0}.vbhtml", "~/Features/Shared/{0}.cshtml", "~/Features/Shared/{0}.vbhtml", }; }} But maybe there's a better way? I'll skip to the solution because I have integrated feature folders in ASP.NET Core and it's just a Razor configuration option:

builder.Services.Configure<RazorViewEngineOptions>(opt =>{ opt.ViewLocationFormats.Add("/Features/{1}/{0}" + RazorViewEngine.ViewExtension); opt.ViewLocationFormats.Add("/Features/Shared/{0}" + RazorViewEngine.ViewExtension);}); With that in place, we can't really use the automatic tooling to migrate but that's OK, I'd rather be very careful about each step in the migration and tackle each problem as they come instead of all at once. We'll use our old friend "Cut and Paste" as our tool of choice.

Migrating the Home ControllerAnother choice we have to make along feature folders is the controller location. In the original application, the controllers lived alongside the views as well. We can still do this in our new application BUT I'm making one small change - the name of the controller I'm correcting to FooController instead of UiController. It's just easier not to get too cute with controller names.

First up is moving the controller by itself. Since I'm just doing Cut-and-Paste, the namespaces won't automatically correct themselves. But again it's just some easy corrections of namespaces:

using ContosoUniversity.Features.Home;using MediatR;- using System.Web.Mvc;+ using Microsoft.AspNetCore.Mvc;namespace ContosoUniversity.DotNetCore.Controllers{- public class UiController : Controller+ public class HomeController : Controller { private readonly IMediator _mediator; public HomeController(IMediator mediator) { _mediator = mediator; } public ActionResult Index() { return View(); } public ActionResult Chat() { return View(); } public async Task<ActionResult> About() { var data = await _mediator.Send(new About.Query()); return View(data); } public ActionResult Contact() { ViewBag.Message = "Your contact page."; return View(); } }} Not too bad, but we still have to worry about the code referenced here and the views. In the next post, we'll migrate the code used by our controller before circling back to our views.

View Details

Posts in this series:

  • Intro
  • Cataloging
  • Empty Proxy
  • Shared Library

In the previous post, we established a beachhead with a completely empty proxy application to prepare for migrating controllers incrementally all the way to production.

There's still one more step we need to take care of before we

View Details

Posts in this series:

  • Intro
  • Cataloging
  • Empty Proxy

In the previous post, we looked at techniques for determining the size and scope of our .NET migration effort, as well as clarifying what our goals should be. But before we start migrating anything, we wanted to "establish a beachhead" and work through any build/package/deployment/production issues with a proxy. So rather than trying to make our first deployment a simple controller, we deployed a proxy that handled nothing and proxied everything.

Our ASP.NET Core App will define zero controllers, APIs, routes, or even any middleware like authentication/authorization. Every request will proxy to the .NET Framework app (we're even skipping this "business logic" common library for now).

The main motivation here is we want to work this app all the way through build, package, and deployment without worrying about the ASP.NET Core app actually doing anything besides the minimal proxying.

The code side of this is actually quite straightforward, we can use the Visual Studio wizard to upgrade our project. We right-click the project and select "Upgrade" where we can upgrade our project to a newer version OR upgrade project features:

The "Upgrade Project Features" selection is new and allows you to upgrade an older-style class library project to SDK-style without changing its target framework. Another nice way of moving in small, verifiable steps.

The first selection is what we want and only offers one upgrade path, but it's the one we want:

Maybe in the future there's a full project migration but I can't really imagine it working except in the simplest of scenarios. And this app has AutoMapper AND MediatR so clearly it's not simple.

In the next prompt we select "New Project" as we don't already have an ASP.NET Core project, and finally the project name/type:

Since app is entirely an MVC application, I went with the first option but it's not hard to add API controllers if you like after the fact. Finally, we can pick the target framework:

This will be up to you on which you choose, but keep in mind that not all features in ASP.NET MVC 5 are available to migrate across to ASP.NET Core. For example, we found that our .NET Framework app used Output Caching but this feature isn't available until .NET 7. That's why we took the time to catalog what features and middleware our app used in the assessment phase.

Finishing this out, we get a second project added to our solution:

There aren't any controllers here. We get a couple services added:

var builder = WebApplication.CreateBuilder(args);builder.Services.AddSystemWebAdapters();builder.Services.AddHttpForwarder(); I'm not configuring any options, just yet. Finally, the reverse proxy middleware is added:

app.UseRouting();app.UseAuthorization();app.UseSystemWebAdapters();app.MapDefaultControllerRoute();app.MapForwarder("/{**catch-all}", app.Configuration["ProxyTo"]).Add(static builder => ((RouteEndpointBuilder)builder).Order = int.MaxValue);app.Run(); The order is important here - we map the default controller routes first and then our forwarder. Any other middleware/routes we want to have our ASP.NET Core app need to be added before the MapForwarder call. This ensures we give our ASP.NET Core app the chance to handle any routes before our forwarder does.

The configuration there is just for the forwarding address which we can find in our launchsettings.json file:

"environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "ProxyTo": "http://localhost:12810"} That URL is the one the .NET Framework app uses for local debugging. Going through this wizard also means that our Visual Studio solution is configured to launch both applications when running.

With this in place, we can run our nearly pointless application:

This is the .NET 6 application's URL, but the content is from the .NET Framework application. Success! And all content is served from the proxy - HTML, CSS, JS, SignalR.

With this in place, we need to get the application pushed through our build and deployment pipeline. This is pretty specific to your situation, but some things we had to do:

  • Package the application using "dotnet package"
  • Create appsettings.FOO.json files for all of the application configuration across our environments
  • Figure out how to poke secrets into our .NET 6 application. The current system used XML poking
  • Configure IIS to include these additional web applications, and pull in the ASP.NET Core 6 hosting module
  • Modify our deployment pipeline to deploy the ASP.NET Core 6 app

Again, we worked in small steps, going from Build -> Package -> Deploy. Since the ASP.NET Core 6 application didn't actually do anything, we could methodically work towards deployment without any big-bang switches. This was especially helpful as our build and deployment servers had pipelines defined outside of our repository (TeamCity).

At the end of this step, we had the empty .NET 6 application packaged and deployed across all environments (our beachhead).

In the next post, we'll take our first steps of migrating the actual code.

View Details

Posts in this series:

  • Intro
  • Cataloging

When I talk with folks about modernization, inevitably the question comes up "OK but how much is it going to cost?" This is never an easy question but first we need to understand what exactly our target state is. That's

View Details

Over the past year or so I've been part of a large-ish modernization effort, both migrating from .NET 4.8 to .NET 6 (the latest LTS at the time) and from an on-premise deployment to Azure. While these two workstreams were largely independent (luckily), we did have some

View Details

I guess it was inevitable, but quite often I see homework problems on r/microservices seemingly assigned in some intro CS class. Peculiarities of that aside, they can be quite fun to work through as thought experiments. One recent one came up:

Hi,
I am working on a school app.

View Details

This release removes all scanning around behaviors, stream behaviors, and pre/post processors. That proved too problematic so you MUST register each of these explicitly with the appropriate registration methods inside AddMediatR. This also ensures that the order of behaviors and pre/post processors reflects the explicit order of registration.

View Details

This is a pretty big release, with a number of breaking changes.

  • Release Notes
  • Migration Guide

Breaking changes include:

  • Depending directly on IServiceProvider to resolve services
  • Making "void" request handlers return Task instead of Unit
  • IRequest does not inherit IRequest<Unit> instead IBaseRequest
  • Consolidating the MediatR.Extensions.Microsoft.DependencyInjection package into the main MediatR package
  • Rolling back stricter generic constraints in various behavior interfaces
  • Various behavior registrations now inside of the IServiceCollection extension
  • Overloads of AddMediatR consolidated into single configuration object

The migration guide includes instructions for upgrading. In addition, this release adds some new functionality, including:

  • Targeting netstandard2.0 instead of netstandard2.1
  • Adding custom notification publisher strategies (for parallel vs. sequential execution, for example).

Enjoy! Or not? Either way, it's out.

View Details

Posts in this series:

  • A Case Study
  • Designing Authentication Schemes
  • Authorizing Client Applications
  • Building the Server
  • Enabling Local Development
  • Connecting External Clients
  • Connecting Azure Clients

Full example

In the last post, we used OAuth 2.0 Client Credentials flow to connect an external client (modeling our on-prem scenario). We can

View Details

Posts in this series:

  • A Case Study
  • Designing Authentication Schemes
  • Authorizing Client Applications
  • Building the Server
  • Enabling Local Development
  • Connecting External Clients
  • Connecting Azure Clients

Full example

In the previous post, we looked at enabling local development by creating an Azure AD Group, assigning our user (me) to that group,

View Details

Posts in this series:

  • A Case Study
  • Designing Authentication Schemes
  • Authorizing Client Applications
  • Building the Server
  • Enabling Local Development

Full example

In the last post, we looked at creating the server application and corresponding Azure resources to secure it. If we try to test our application locally, we'll quickly run

View Details

Posts in this series:

  • A Case Study
  • Designing Authentication Schemes
  • Authorizing Client Applications
  • Building the Server
  • Enabling Local Development

Full example

In the last couple of posts, we looked at authentication and authorization options and picked some strategies for each. With those pieces chosen, we can now set about building

View Details

Posts in this series:

  • A Case Study
  • Designing Authentication Schemes
  • Authorizing Client Applications
  • Building the Server
  • Enabling Local Development

Full example

In our last post, we chose the OAuth 2.0 Client Credentials grant scheme to authenticate our APIs and daemon applications:

API and daemon application calling an internal API

View Details

Posts in this series:

  • A Case Study
  • Designing Authentication Schemes
  • Authorizing Client Applications
  • Building the Server
  • Enabling Local Development

Full example

In our last post, I walked through my real-world scenario of needing to follow Zero Trust principles in securing microservice-based APIs inside (and outside) Azure. For our sample system,

View Details

Posts in this series:

  • A Case Study
  • Designing Authentication Schemes
  • Authorizing Client Applications
  • Building the Server
  • Enabling Local Development

Full example

Recently, I was on a project where the company had a big push towards a "Zero Trust" security model. This concept was new to me, and I'd always been

View Details

Posts in this series:

  • Evaluating the Landscape
  • A Generic Host
  • Azure WebJobs
  • Azure Container Instances
  • Azure Functions
  • Azure Container Apps

Well it's been a while since we visited this! I intended to follow up with a post on Kubernetes but to be honest, Kubernetes is far too complicated to get

View Details

I've been pointed at this post from various sources, and I thought I'd take the time to address the criticisms one by one. Not because I personally care too much, MediatR evolved over many years to solve the problems my teams faced, but since others seem to care, here we

View Details

My AutoMapper search alerts brought me this gem this morning:

Hey... let's use automapper to generate passwords.

This post illustrates basically all the wrong reasons to use AutoMapper. Here's the code minus the unimportant bits:

public class RequestProfile : Profile { public RequestProfile() { CreateMap<Request, NewAccount>() .ForMember(d => d.

View Details

From the previous post on building NServiceBus metrics, I've pushed a new version of the NServiceBus.Extensions.Diagnostics and OpenTelemetry packages:

  • NServiceBus.Extensions.Diagnostics NuGet
  • NServiceBus.Extensions.Diagnostics ReadMe
  • NServiceBus.Extensions.Diagnostics.OpenTelemetry NuGet
  • NServiceBus.Extensions.Diagnostics.OpenTelemetry ReadMe

As before, the OpenTelemetry packages are just small wrappers around TracerBuilderProvider.

View Details

The release of System.Diagnostics.DiagnosticSource version 6.0 a few months back brought something entirely new to the library - support for OpenTelemetry Metrics. Since this package releases out-of-band from the .NET 6 SDK, it also means you can use these new metrics APIs in any application targeting .NET

View Details

Waaaaay back in the ASP.NET Core 3.1 days, I wrote about increasing the cardinality of traces using Tags and Baggage. You could write code in your controllers, filters, or application code to be able to add custom information to traces to help find these traces more effectively:

[HttpGet]

View Details

Last week I pushed out the 2.0 release of NServiceBus.Extensions.Diagnostics package:

  • NuGet
  • Release Notes

And related packages:

  • NServiceBus.Extensions.Diagnostics.OpenTelemetry
  • NServiceBus.Extensions.IntegrationTesting

The biggest feature change was to allow behaviors to be able to modify the original activity started by accessing an ICurrentActivity from the

View Details

An issue I see come up quite frequently, much to the chagrin of DI container maintainers, are problems of complex generics edge cases and how they come up in MediatR. In fact, more than one container author has demanded some kind of recompense for the questions received and issues opened

View Details

I pushed out a new version of Respawn today:

  • Release notes
  • NuGet

Enjoy!

View Details

Well, it is, sometimes. It depends.

I often get pushback on MediatR for using service location for resolving handlers, often getting pointed at Mark Seemann's post that Service Locator is an Anti-Pattern. And for all of the examples in the post, I agree that service location in those cases should