Show HN: Mapperly – A .NET source generator for object to object mappings
github.com
github.com
All that said, given these mappers exist, the fact this Mapperly does ahead of time code generation is an advantage over the previous generation of generators which had to use reflection.
So its something like this,
target_dto = { k: MyDbModel.get(k) for k in MyTargetModel.keys() }
Writing this is a big issue with simple static languages (ie., those without much compile-time programming).This deficiency should really be addressed at the language-level. Ie., what you want is structural typing in the view-layer, and a means of restructuring (ie., filtering) the original db object.
So,
renderView(myDbModel as {Just,The,Necessary,Fields})
I'd imagine with C# Shapes, Extension Methods and Pattern Matching, you'd be able to do roughly this -- but i'm not sure of the status of "shapes" (ie., typeclasses) in the C# RFC process.But yes, structural typing and other interesting typesystems can make this first class.
I didn't realise serious work had been put into typeclasses for C#. Interesting, but unfortunately it seems to have stalled.
And after the number of times I've started a job and asked about something that looked arcane, odd, and difficult to work with and gotten the reply "well we used to have this code generation tool", I've come to the conclusion that I don't wish to rely on code generation tools either because the incentives to build code generation tools and the incentives to maintain them seem to be severely misaligned such that building on a foundation of code generation tools seems to be equivalent to building on a foundation of quicksand.
I'm curious, what do you use for this use-case, if you have it, in your company?
(of course, everything is dependent on the particular project's needs)
It's nice having separate classes to describe the request & response bodies because this lets you automatically generate OpenAPI descriptors which can then be used for client generation and so on.
When your domain objects are small it's easy to just manually copy the data between your domain entities and request & response objects. Once your objects grow it's convenient to just automate the mapping in a way that still gives you compile-time correctness checks.
It all clicked for me when I read somewhere: "The best way to define a data structure is a class, that's what they are".
So now you can create Response and Request versions of your classes and use AutoMapper to convert them.
When someone green comes into my Domain Driven project they always have the same question - why are there so many versions of your classes? In my project I have the aforementioned versions of those classes, but also the auto-generated Entity Framework classes in the Repository - which mimic the database structure - but it's not 1:1 (e.g. a many to many relationship will most likely not have a Domain model for the joining table - unless it has its own non-Foreign Key fields defining that relationship). But I don't use AutoMapper for that part - just a lot of code - maybe one day.
Oh, and then I have at least one more version in Typescript!
Yes, they don't handle 100% cases and in many cases we need to write custom mappers, but for the 80% cases it does work it really eliminates a lot of tedious manual code. This is specially important if your codebase is evolving fast and domain model goes through multiple iterations.
Also, something that is often overlooked is that mappers are composable. So you could define a custom mapper for a specific class that needs special conversion, and then have MapStruct configured to use it in all the twenty classes that use that type without having to write those twenty mappers yourself. Its a big win.
When I'm building something in a more domain-driven approach, which I usually strive for, tools for domain model<->dto mapping (assuming the separation between application and domain layers) does not make much sense. Because even when I have a domain model/dto pair with almost the same attributes I will be creating the model via a well named factory method in the domain layer, with notification style validation. And for changes, even for HTTP PUT, specially for HTTP PATCH, I'll be updating a subset of the attributes and for that I'll be using proper entity methods with, again, notification style validation. On both cases manual handling is the way to go.
If you're mapping lots of open fields to your domain models... it's not domain driven. Might be a choice, but should be conscious.
And if a) automatic mapping makes sense for a subset of the application, be it for api dtos or internal matters (you might be dealing with dtos internally for some integrations), and b) we'll really will have a one liner (including a one liner config) instead of mapping N attributes in multiple situations, ok, but I'll consider to define some sort of simpler Mapper interface and put the tool behind it without leaking its details for application and domain layers.
Therefore, thank you for making this. Automapping is a sign of incorrect abstractions and unarguably bad solution architecture but since we are still forced to deal with such, doing so with speed and without reflection is always welcome.
> without reflection
Source generators are not reflection.
In such cases, having an extra abstraction is both redundant and an anti-pattern that made sense in the age of large monoliths where the scopes/contexts/features where segregated by modules.
Today, where modularity and protection of abstractions is no longer of concern because many teams can easily maintain up to 10 or even 15 (micro)services, the bias towards a certain solution style/architecture that is full of unnecessary abstraction layers, boilerplate and patterns that violate locality of behavior like there is no tomorrow is something that makes using C# much less attractive than warranted.
Ultimately, it comes down to the fact that unlike Go, C# is more than 20 years old, and while it builds upon ideas that were ahead of its time back then like async/await, LINQ and some other, it also suffers from "tradition" which can be easily seen in community resistance to rely on top-level statements (aka Python-style Program.cs), religiously following the rule "one file = one class" (even if it's just 'record User(string Name);') or simply overall creating 5-project solutions for something expressible in 3 .cs files.
Keeping in mind Chesterson's fence, I do acknowledge that the above is a result of likely reasonable and well-thought-out choices at the time, but it does not mean the circumstances haven't changed either.
You see in C# and Java that people love splitting classes into files (and are forced to by automated auditing tools - my last big project suffered from that, even as Lead I had my hands tied). Navigating such projects is horrible outside of tooling (such as in github) but fine if you always work within the IDE.
In Python I'm used to wading through classes of >1k LOC which are only slightly related because they have the opposite habit. I've yet to find a way to work around this.
Honestly I'm not sure what I find worst: it really depends on the project or the domain. If I had to chose I think that I'd prefer "too many small files" to "one huge file" as the latter has an extremely high risk of turning into a "big ball of mud" and it's what the clueless beginners do. That and trauma of having to maintain some old Visual Basic system where that was the norm.
Although to be completely honest: I would much rather that the AST was the main interface for code. The fact that our tools are based on text is a relic of the past.
In the right hands - so with strong progressive leadership - it wipes the floor with other ecosystems.
Also changing the app back to old format is "Ctrl + ." -> "Change to Program.Main" away.
Developers would find a gripe with one new feature or another, even if it makes achieving a particular goal easier and less painful, simply because change is very uncomfortable to many.
Your arguments reject complexity, then, in the next breath, endorse it. Micro-services are the enterprise bloat of our time (and likely the worst example of all the bad ideas that have circulated in that space). Small departmental apps used by 15 people require 34 micro-services (all hitting the same db tables of course).
To give a better example, recently on HN there was a discussion on self-hosting Bitwarden and its respective implementations. The official one[0] is written in C# and uses 15ish containers. The alternative one[1] is written in Rust and is a one application. I think both have their merits, since the former is used to serve possibly millions of users at this point, while the latter is best utilized in self-hosted home or SMB scenarios.
What exactly does Go do here, besides being grossly more verbose and less expressive than C# (and even Java)?
Sure, there are edge cases to nominative typing systems, and some of these particular cases may be better expressible with a structural one, but that’s another discussion to have.
But if there are little of those, no need to make them prematurely. Field mapping is something for EF Core mapper configuration or repository implementation to worry about, no need to do the job twice.
You often don't need separate "Host" and "Core" projects either. And if you ever need to migrate to a new hosting solution, it happens maybe once during application lifecycle, and even then it requires some effort.
Replace-all and IDE tools work just as fine if not better for this, no need to inflict easily avoidable pain simply because it looks consistent with "other" solutions that were written 10 years ago.
Yeah mate, let's create a generic repository over the top of EF's perfectly decent DbSet<T>, just in case we ever decide to switch ORMs!
Why would the customer need to know about the cruft? The view/UI shouldn’t be exposing what’s not needed. Same with the DB, frameworks like MyBatis put the mapping where it belongs.
Then that pattern can be extremely effective - in fact, that is exactly the reason that some of us were pushing the patterns you see today. It was especially effective back before we had strong typing in frontends as it saved a lot of time otherwise wasted on QA or typo's in the frontend.
While manual handling is required when you have proper domain models with controlled creation and change operations, these tools can be useful for other matters.
That’s yet to be seen, otherwise I wholeheartedly agree with your statement :)
I can understand that this might be intrieging for newcomers. I've certainly learned the hard way that the only thing worse than manually writing mapping code is doing it automatically at runtime.
The problem with automapper is that it throws the compiler out the window and invites all the bugs that would normally not be there.
Mapping code is still your code and should receive the same care as your fancy services.
I hate automapper, and I hate this too, just less because at least it has the potential to catch bugs at compile time… I think.
Some people really want to write libraries, I guess.
I've seen projects when Automapper was the main reason for the cold startup of an app, which caused terribly slow "write->build->run" developer experience.
So it's not only a problem of runtime exception vs compiler errors. Performance also matters.
> Mapping code is still your code and should receive the same care as your fancy services.
I've also seen project where all mapping code was written by hand and it was required to write unit test for it. It was also not pleasant.
“Yeah we may risk introducing more bugs but it’s so much nicer to develop now!”
It’s also OK to write libraries.
Some people really want to write mean comments, I guess.
That said, I have failed to find a practical use case for source generators in my day-to-day work. Reflection is very accessible and we have managed to avoid severe penalties of this on most paths so far.
Has anyone found other good use cases for source generators in their projects?
Source generators are compile-time safe. Well, unless they generate unsafe code, of course, but most of them shouldn't and don't.
They're also safer in a broader sense in that any "implicit" changes will show up in the version control history, if you commit the generated code (which you should!).
Performance is not the main issue, it's future changes breaking something that can't be checked at compile-time. Source generators can't break in this way.
But, now that I use rider instead of VS. That is the main feature I miss.
I will be trying this out. Thanks OP.
I actually agree with you that an API response ought to be a different class, but you probably also want to consider it more carefully than using automation to generate the mapping.