Four libraries that half the .NET codebases I've worked on depend on changed their licence terms inside a year. Fluent Assertions, MediatR, AutoMapper, MassTransit. None of them did anything wrong. Maintainers are allowed to get paid, and after a decade of unpaid support I'd have done the same. But it exposed something teams had stopped thinking about, which is that free isn't free, and a lot of these libraries solve a problem you didn't have.
Overengineering is rampant, and a package reference is the easiest way to do it. Nobody reviews a line in a csproj the way they review 50 lines of code, even though the line in the csproj is the bigger commitment.
Three libraries and the problem they claim to solve
AutoMapper
The pitch is "I don't want to write mappings". The mapping it saves you looks like this.
x.Name = y.Name
Most DTO mappings are that, a few lines of assignment. What you get in exchange is configuration you now have to learn, ReverseMap() and ForMember() and profile discovery, and a debugging story where the answer to "why isn't this property mapping" lives in a convention rather than in the file you're looking at. The mapping itself is fast, since AutoMapper compiles expression trees rather than reflecting on every call. The cost was never the CPU. It's that a rename in a DTO breaks a mapping silently at runtime instead of loudly at build.
Most of the AutoMapper usage I've seen could be a constructor or a method. The deeply nested cases might justify it, and even then hand-written mapping is usually clearer.
MediatR
The pitch is "I want a clean separation between commands and queries". Most applications don't need CQRS, they need a service class.
What MediatR buys is indirection. Go-to-definition on Send lands you in the library rather than in the handler, pipeline behaviours run in an order defined somewhere you aren't looking, and registration is assembly scanning that fails at runtime rather than at build.
// Instead of MediatR
await _mediator.Send(new CreateOrderCommand());
// Just do this
await _orderService.CreateOrderAsync(request);
If you need decoupling later, an interface gives you it. You can add a mediator to a codebase that has service classes. Removing one from a codebase that has 200 handlers is a project.
MassTransit
The pitch is "I need robust messaging with retries and sagas". If you need sagas, you need something like MassTransit and you should use it. If you are publishing a message and reading it at the other end, raw RabbitMQ or Azure Service Bus is fewer moving parts and one less framework whose upgrade path you own.
Start simple. Reach for the framework at the point you need the workflows, not at the point you draw the architecture diagram.
The bill arrives later
The licence can change overnight
- Fluent Assertions went to a paid model for commercial use at version 8.
- AutoMapper and MediatR launched commercial editions in July 2025, covering version 15 and version 13 respectively. Earlier versions stay on their original open-source terms.
- MassTransit is doing the same at version 9, with v8 and earlier staying free.
- Redis and Elasticsearch both moved off pure open source, and both got forked by the cloud providers for their trouble.
The pattern is the same each time. Existing versions stay free, so nothing breaks today, and you are now on a version that will stop getting security fixes at a date somebody else picks.
The maintenance burden
Upgrades carry breaking changes and, now, licence changes. Abandoned projects leave you holding a dependency nobody is patching. And a maintainer can do worse than walk away, which is what the faker.js author demonstrated by pushing a deliberately broken release to every project that trusted him.
Just one more dependency
Each library looks harmless on its own. Then you're pinned to an old version because upgrading is too risky, your restore is pulling 500 transitive packages, and a CVE three levels down the graph is now your production incident.
When to skip it
Manual mapping over AutoMapper.
public OrderDto MapToDto(Order order) => new()
{
Id = order.Id,
CustomerName = order.Customer.Name,
// Explicit is better than magic
};
Direct calls over MediatR.
await _orderService.CreateAsync(request);
Raw publish over MassTransit, for the simple cases.
channel.BasicPublish("orders", "order.created", message);
None of that is clever. That's the point. It debugs by reading it, it has no licence, and the performance question never comes up.
Open source earns its place on foundational things you would never build, PostgreSQL, the .NET runtime, Serilog, an ORM. It stops earning its place on trivial problems, on your core business logic, and on any project with a history of changing terms.
A pragmatic approach
If a library's functionality could be rebuilt by a team smaller than two pizzas can feed, consider doing it in-house.
If you still want the library, wrap it. One interface you own, one implementation that calls the thing you don't.
public interface IDtoMapper
{
TDestination Map<TDestination>(object source);
}
public class AutoMapperWrapper : IDtoMapper { ... }
That's the difference between swapping an implementation and refactoring an application.
Then audit what you already have. dotnet list package --include-transitive shows you the graph you actually shipped, and FOSSA, Snyk, Mend or Veracode will tell you what its licences are today.
To finish
Every dependency is a bet that somebody else will keep maintaining, for free, something your product needs to work. That bet has been fine for years and four libraries just called it in. Ask whether you took the dependency because you need it or because it's the default, and whether you could replace it with 50 lines you own.
The cheapest dependency is the one you didn't take.