Hacker News new | past | comments | ask | show | jobs | submit login
Show HN: Mapperly – A .NET source generator for object to object mappings (github.com/riok)
53 points by latonz on Jan 12, 2023 | hide | past | favorite | 69 comments



I've never really understood the use case for these mapping generators. Every time I've seen them used (MapStruct in Java, AutoMapper in C#) the mapping configuration eventually ends up being so complex that just manually writing out the mapping would've been just as simple and arguably simpler to understand.


I've used Automapper a few times, in Enterprise software which takes separate layers to a dogmatic extreme. Automapper always felt like a code smell: if the layers are so trivially mappable, should they really be different layers? Alternatively, isn't automapping a violation of the philosophy of separation of layers?

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.


After the number of times I've seen AutoMapper absolutely abused and had to track down some really head-scratching bugs introduced by it, I've come to the conclusion that I'd rather just eat the pain of writing the mappings by hand.

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 assume the automapper is subsetting on the target type's structure, right?

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.


Thanks for this thoughtout reply. Automapping can also be used for renaming, and simple type conversions.

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.


In webapps it's often very useful to accept a subset of a domain entity as an APIs request body, or to return it as a response body.

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.


Agreed. If you've come along the journey of first using the json attributes on your Domain objects to try and define your return but then get stuck the first time you have another API call returning a different subset of fields.

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!


All those KLOCs! Such productivity!


AutoMapper is almost universally misused/abused. It's intended for use cases where 98% of the model's properties can be mapped automatically, typically where the result model isn't doing anything except excluding a few properties or maybe flattening a nested object. For anything else (i.e. any scenario involving significant changes in the data shape or structure) IMO you are better off writing the mapping by hand. But once devs get hold of AutoMapper, every mapping problem becomes a nail...


We ran into a problem where AutoMapper changed it's approach between versions for a way we were using it. We then got to spend the next few weeks updating our code because of it. I'm pretty sure that all the time we "saved" by using it was lost.


My project had the same issue. I ended up stripping out AutoMapper and manually mapping my types. In the end, it was much easier to determine exactly what was going on, and now I have one less third-party dependency.


Most of the time I don't need automatic mappers, but it's nice to have good, fast tools available.

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.


I banned Automapper from my company.


For me, it's been great for mapping the Domain objects to API Response objects. I wouldn't recommend using it to map your Dto objects to your Domain objects. At the end of the day it helps to solve the problem of multiple API responses sourced from the same Domain object. For example, the "Product" object in a list of "Products" may not have the same fields as the "Product" object in the "Product Details".

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)


So did I. Do the napping manually and let the type system actually help you instead of being invisible.


I haven't used it for years and I have never looked back! Even in extremely large systems, manually writing a mapping layer is quick (although admittedly obnoxious) and has helped me identify issues so much easier.


It's been many years since I was a (.NET) developer and therefore since I used automapper. Are there any VS plugins that take the tedium out of this mapping? Right click on a class, choose a class to map it to, and it creates the boilerplate code for the mapping layer?



Thank you. Now please ban it from planet earth.


They are incredibly useful when you need to rely on code-generators or need to deal with multiple type-systems. In our case row types were derived from database schema and api layer DTOs were derived from Protobuf schemas (neither were owned by our team) - it was convenient to have something (in our case MapStruct) to map between them.

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.


It is sad we still live in the enterprise world where having both "SomeClass" and "SomeClassEntity" isn't grounds for rejecting a PR that dares to do this.

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.


I'm curious, why would this be bad? I've generally always seen it as best practice to create classes in your domain model and try to write your ideal code and then have separate classes/DTOs that are used for communicating outside your domain model such as reading/writing to a database using an ORM or responding from an API with your ideal model for the consumer (since more often than not consumers don't need or want all of the cruft inside the domain model and your DB will likely have stored the information in a different format in a large system). I always love thinking of ways to simplify architecture though and I absolutely hate having classes/records that looks so similar though so I'm intrigued about a simpler solution if there is one? Domain Driven Design practices really encourage a bloated/complex architecture so I try to avoid it if possible but more often than not business requirements end up becoming so complex that I immediately fall back to those abstractions when the logic starts to get complex.


Abstractions are useful to make a box and put horrors of edge case handling or business requirements variability in it :)

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.


The amount of time C#/.NET developers waste on abstracting away all kinds of shit, creating three different layers for "separation of concerns", having 10 projects in some basic web app solution.. my god, they truly are suckers for punishment.

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!


> customers don’t need or want all of the cruft inside the domain model and your DB…

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.


A simple set of dtos in the application layer, defined around their respective application services (use cases if you will) is ok. Required for most bounded contexts/microservices. You properly organize application and domain layers. It does not need to be verbose. The problem is when people define dtos on the http infrastructure, then similar dtos on the app layer, domain models (often with all attributes being public and behavior scattered elsewhere), then dtos for database schemas... AutoMapper looks useful in these scenarios, but it's the wrong solution for poor design.

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.


As always, it depends. Are you working on a CRUD system and are you using models to generate your UI/forms (or to generate a service layer for your SPA / Android / iOS apps)?

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.


In many cases DbModel is not exactly the same as AppModel and ViewModel. In typed programming languages automappers are rather useful.

> without reflection

Source generators are not reflection.


Yes, the above comment was addressed at most popular use case - that of back-end services where entity to class map 1:1. In fact, there is already mapping in defining DB field types (if non-default) in model registrations consumed by ORM. Worst case it is always an option to project within DB query itself, sometimes even "cheaper" too.

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.


I've been programming forever now and have written a lot of C#, TypeScript (/JS) and Python - and I much prefer the approach used in ServiceStack and in Vue.js of putting the "things which belong together" in one file than the alternatives.

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.


While some tooling has trouble with many small files, I find it just as debilitating to navigate a huge C file for example, that mandates heavy jumping between different positions even in IDEs. Sure, I do know about multi-pane editing, but my brain prefers the “tab corresponds to file” abstraction.


Same here.

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.


>> teams can easily maintain up to 10 or even 15 (micro)services

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).


It all comes down to scale. I'm not endorsing splitting off logic into different applications for the sake of it - it is no better (usually worse) than splitting logic into separate modules for things that could've been "together". 34 microservices per just 15 people sounds like a lot of trouble unless those are really "lean", have little to no boilerplate and managing infra-side of things is automated away or outsourced to dedicated platform team.

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.

[0] https://github.com/bitwarden/server

[1] https://github.com/dani-garcia/vaultwarden


The latter has API equivalency, but that is not the same thing as feature parity. Plus, you have very different concerns when you want a small self-hosted app, vs a more “traditional” deploy model where integration and the like are more important, shaping the way you program.


Services should be factored along scaling dimensions, not domain or team size. Ideally scaling and domain dimensions will be coupled to some extent, but that is not always the case. The popular idea that needs to die is that services are somehow tied to team size.


BTW have you seen the new Node / Go style top-level statements in the .net 7? Microsoft have been doing some fantastic work, they've constantly been taking the best bits from other ecosystems.

In the right hands - so with strong progressive leadership - it wipes the floor with other ecosystems.


Top-level statements are actually my single biggest beef with .NET 6 and 7, specifically because Microsoft decided to make them the default for any new console apps. And they didn't even add a flag for reverting to the old behavior until .NET 7, their guides for .NET 6 if you actually wanted a proper Program.cs file were literally "Generate is with .NET 5 and then up the target framework to .NET 6." This broke over a decades worth of guides/tutorials for beginners, right around the time I was trying to help a younger relative learn to code.


That is the goal. Change is likely to cause documentation and knowledge to become outdated.

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.


Yeah this! I write a lot of console apps and I absolutely love this change as it simplifies and removes a lot of cruft. Which means more people will make more console apps.


> unlike Go

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.


Sorry, this was meant to refer to simplicity and focus of Go at solving problems it was designed to solve. Not that it doesn’t suffer from its own kind of boilerplate hell (the verbosity you mention), but it does not take away its upsides.


(and as an aside: Python is very much VisualBasic++, I actually kind of like using it but it's just tiring putting up with the amateur level of other developers and the flakey ecosystem)


Something that should not be needed was just improved.


> was just improved

That’s yet to be seen, otherwise I wholeheartedly agree with your statement :)


If a type can't be mapped this should now fail at compile time instead of at runtime - which is an improvement.

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.


I like how everyone here and the repo’s readme talks about performance as if the problem with Automapper was the fact that it was making your enterprise app that waits N seconds for a db query and relies on caching to be even remotely usable, slow.

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.


> The problem with automapper is that it throws the compiler out the window and invites all the bugs that would normally not be there.

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.


Yes, trade code safety & quality for programmer comfort. Amateur mindset, much like the mindset behind automapper.

“Yeah we may risk introducing more bugs but it’s so much nicer to develop now!”


Performance is a feature. For certain use cases, it’s a top priority. It’s OK to let users know that your library is designed with performance in mind.

It’s also OK to write libraries.

Some people really want to write mean comments, I guess.


oh yes, I will forever be mean to automapping libraries


The ability to use source generators in .NET did seem like a really powerful capability when it was first announced.

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?


Reflection is inherently unsafe. Even something as relatively simple as a JSON de/serializer can blow up in your face.

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!).


> Reflection is very accessible and we have managed to avoid severe penalties of this on most paths so far.

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.


Not using reflection also allows safer code trimming.


The number one reason is if you want to make your code AOT friendly, followed by not having to write all the boilerplate required by stuff like INotifyPropertyChanged nor depend on MVVM frameworks for that.


Regular Expressions and System.Json support using source generation for performance gains.


This has always been a pain. One recent improvement has made it better though. The new VS IntelliSense that generates code for you creates mapping MUCH easier as it fills in the rest of the shallow copy for you.

But, now that I use rider instead of VS. That is the main feature I miss.

I will be trying this out. Thanks OP.


What are the advantages over AutoMapper?


Automapper generates all mappings at runtime with reflection. Mapperly in contrast uses roslyn based source generators and generates all mapping code at compile time. This leads to improved runtime performance with less allocations [1]. Since no reflection is used at runtime, the generated code is completely trimming save and AOT friendly.

[1] https://github.com/mjebrahimi/Benchmark.netCoreMappers


And what's the advantages over Mapster? (haven't used it yet, but I see it's mentioned often as a "better Automapper" alternative)


Never used Mapster myself, but as far as I can see, Mapster does not provide a Roslyn based source generator. By default, it seems to create mappings at runtime (if not using the Mapster Tool to create mappings at build time).


Even though AutoMapper of course uses reflection to construct mappings, executing mappings does not involve reflection if not necessary. AutoMapper builds expression trees, compiles them on first use, and uses the resulting function to perform the mapping. If your mapping involves things only accessible through reflection, then the compiled mapping function will of course still have to rely on reflection.


Those are pretty compelling advantages. Thanks for sharing.


Why not just have no mappings and reuse the same objects?


So you are happy with the same class that is used to represent the row in the database including potentially sensitive data also being used in responses to API calls?


For rapid development, potentially yes, as is easy enough to slap [XmlIgnore] and [JsonIgnore] on properties you don't want serialised in responses.

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.


or better yet, directly in GUI layer :)


It can be useful to distinguish between domain objects and objects that you use at the edge of your service, like on the API. They might have different annotations or you might want to evolve them differently, e.g. your domain objects might change while you want to keep your API the same so as not to break your consumers. Things like that.




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: