3 .NET 'Best Practices' I Changed My Mind About

preview_player
Показать описание

Hello, everybody, I'm Nick, and in this video, I will talk about the 3 practices that I used to consider good in the past, but I actually don't use in my own code anymore.

Don't forget to comment, like and subscribe :)

Social Media:

#csharp #dotnet
Рекомендации по теме
Комментарии
Автор

A few things I used to evangelize that I no longer do myself:
1) "Interface all the things!" - I used to ALWAYS start with the interface. This lead to having a lot of interfaces that didn't really need to exist. I still frequently end up with an interface, but by starting with implementation first it gives me a better sense for what my interface actually does and does not need.
2) "Clever" code. Unless I have a very unique problem to solve that is best solved by a clever solution I try to avoid cleverness now. Even if I could maintain it well myself, I too often wasted time helping other developers understand code that was needlessly complicated. A major part of that was ditching the use of reflection in my own code almost entirely.
3) Multiple projects. I picked up this habit from some senior engineer many years ago. He would break everything up into very very small projects. He always claimed doing so was future-proofing the code, helping with build time, making it more maintainable, etc. I never saw those benefits materialize. I now try to always start with a single project and use folders (and even those I use sparingly).
4) Making everything internal. This was a habit I had in place when I started with #1. Every implementation had to be internal. The only type that could be public was a single master factory class for creating instances of other factory implementations for the various types I would need. In that way, consumers would always deal only with public interfaces and a single static public factory class. It felt right at the time, but the unnecessary headaches it caused make me feel bad that I pushed it on others.

Basically, I now write code again like when I was first learning. But I do so with much more confidence and critical thinking and never write anything I don't have to write.

bradenb
Автор

For the guard clauses: the domain code is responsible of its internal state, therefore it must check its inputs. Of course the application code must perform validation so that it does not even attempt to misuse the domain objects, but if it does then the domain objects should complain via exceptions (which are then exceptional and show to the caller that the program is wrong)

Fred-yqfs
Автор

Personally, what I dislike the most is grouping code by non-business funcionality, like having all mappers, validators, exceptions, etc. together in some folder. Even the idea of having separated commands from queries or requests from responses looks to me wrong (i.e. I prefer to have together in a folder UpdateCustomerRequest with UpdateCustomerResponse than UpdateCustomerRequest with UpdateProviderRequest just because the two are requests). The suffix (request, /response, command/query, mapper or validation) already separates them, so folders are not needed for that.

nmarcel
Автор

I think you summed a lot of this up nicely with... "I like visibility more than magic".

My coworker has been experimenting with using CoPilot as his code generator to automation the initial creation of some things, like the mapping of domain objects to DTOs. We were trying to interface with a third party, and the auto-generated code from importing the WSDL was not working and because it was auto-generated it was hard to edit. So we took the specific parts of the calls we needed, grabbed XML samples and then used Copilot to generate some objects and mappings and a client around them. It worked fairly well. Very easy to read code, not so much auto-generated as generated once and then maintained.

minnesotasteve
Автор

I'm so glad I'm not alone in the camp of "I had to deal with my own shenanigans that I thought was cool and clever 6 months ago, and it was hell"! It has been feeling very barren here for some time :)

To me, this is a sign you've truly grown and matured as a programmer. Thank you for this video 🙏

Pookzob
Автор

(1) 0:43 - 3:07 - Magical "DI Installer" using Reflection vs. Extension Method (Visibility, Order of Execution)
(2) 4:46 - 6:57 - Not using a Mapper vs. manual approach
(3) 7:50 - Guard clause
This is a great video breakdown. Happy to see you take on some old fashion, more manual processes. Yes, i'm old fashion

dsuess
Автор

Love seeing how other developers evolve in their practices. Thanks for sharing.

StephenMoreira
Автор

Avoiding magic and being explicit, moving runtime errors to compile time, using types to represent expected failures. These were all part of my journey from C# to F# and I've never looked back (I watch some of Nick's content because he talks about general .NET concepts). Thanks for the video!

AnythingGodamnit
Автор

I have never been a fan of automated handlers for repetitive work, they create an area where control is lost into a magic world of dependency.
Especially for code where you can still be in control.
Sometimes rapid development leads to tedious support. You have to always consider that you did not cater for every aspect a user might exploit and if you lose visibility of your data inside an invisible process it becomes difficult to find and fix the root cause.

Be considerate of those that will maintain your code, they do not have the privilege of seeing your thought process.

ettiennedebeer
Автор

Nick: Hello, everybody. I'm Nick and in this video...
Captions: hello everybody I'm naked in this video...

RobinHood
Автор

I also started using automapper a while back but after having had to deal with the frustration from debugging when property names no longer match, I just wrote my own generic interface IMapper<TSource, TDestination> with an abstract class on top to wrap null-handling and it works great. There's almost no magic at all except from some reflection to actually find and register all the mappers. Works great!

Something else I've stopped doing is following the idea that you might as well combine data-access classes with domain classes. And then you have scenarios where you *have* to load a big-ish "aggregate" just to use a really small part of it. When I first tried writing really simple data-access classes for EF core with almost no navigation properties and simpler domain-classes it just clicked. It's sooo much easier having that natural separation between the layers. When I don't need a whole aggregate to do something, I just don't load it. And I still don't have to mess with lazy loading or explicit loading that turns into situations where you have no idea if the property is not loaded or just loaded but actually null.

Автор

Nick, provide an example of how nowadays you validate invariants instead of guard clauses (of any kind).

bfg
Автор

I agree with all except guard clauses, when doing DDD I still think guards have some value in ensuring your aggregates aren't instantiated in a bad state.
That besides your database is basically your last line of defense for data corruption. Obviously any input validation should have happened prior to this, so these exception are still exceptionall.
I find it cluttering the code if you need to pass a result object back from your domain model all the way back to the Controller.

rzaip
Автор

I see a lot of things happened lately to simplify our developers life. We are using more simple things and increase readability of the code and in the same time maintainability. I wish a lot of people should look at your videos and also using common sense during coding. I hope this trend will continue in the future.

dxs
Автор

All excellent advice - it takes experience to see those magic approaches as not simplifying the code, but in fact just hiding complexity.

Bite the bullet and handle the complexity as close to the compile step as possible, as explicitly as possible, and wherever you can make invalid state unrepresentable.

(Guard clauses being a hack to handle the possiblity of invalid state)

orterves
Автор

Agree with all your suggestions especially manual mapping! That is where one can introduce backwards and future compatibility with intelligent defaults where required. Obviously logged once only if applied.

roderick
Автор

I totally agree! Just have an observation about #3. Throwing an exception brings us a StackTrace which is very useful sometimes. When we use a Result pattern or something like discriminated unions we might have lost an original source of the error

valera
Автор

Spot on, Nick. I have transitioned away from these same practices as well for the exact same "manual" alternatives. I find it hard to trust the "magic", even though it's cleaner, and faster to implement. Until you need to debug. Thanks for video!

nickandrews
Автор

I completely agree. I started using Automapper, but once they went to the paradigm where it was supposed to be a DI item, I changed my mind and started using it less. And once I started using it less and started building my own mapping functions (usually on the DTO classes as either the constructor or a conversation operator / function), I realized I liked it better. It also allows me to do a find reference and find every case where a class variable is used. No longer are those hidden behind automapper calls.

And I really do not like exception handling for errors. I use it where I have to, but most of the functions I write do not throw exceptions, they handle the exceptions. I find try/catch just too clunky to produce elegant code. But I am one that likes to use Promises on the front end rather than await /try/catch as well.

gveresUW
Автор

Nice video! Agree with them all. It's nice to see that we evolved in similar ways

CarlosRodriguez-ghow