none
Patterns and Practices - Super-DRY Development for ASP.NET Core RRS feed

  • General discussion

  • Don’t Repeat Yourself (DRY) stands as a foundational principle of good software development. In this article, Thomas Hansen shows how to leverage DRY concepts to create a software architecture that enables the creation of an entire application using minimal code.

    Read this article in the June 2019 issue of MSDN Magazine

    Monday, June 3, 2019 5:58 PM
    Owner

All replies

  • Hi, working my way through this article rebuilding it 1 line at a time.  I have a question though, as I think about CleanCode architecture, Mediatr and Fluent Validation.  I am wondering how I could integrate specifically Fluent Validation for the domain objects that would be defined prior to using the generic crud?  How could one provide Fluent Validation for each object prior to saving? Any thoughts?
    Sunday, June 9, 2019 4:55 PM
  • Notice, I had to search for Fluent Validation, and I only had a minute to rapidly skim through the concept, so my answer might be wrong - However, I imagine it must contain some way to manually trigger validation (CustomValidator.ValidateAndThrow?), which would probably be your friend here ...

    Then comes the question of where to implement this? As in on your database model or your view model, at which point I'd probably choose to do it on the view model myself, since the view model is the only "input type" in your code, and the input is what you'd normally want to validate. This implies that you'd probably want to implement it in your Controller class, since at the point you reach your service implementation, your view model no longer exists. The configuration could probably be done in an implementation of the "IConfigureServices", which will be automatically invoked during startup, at which point you could use the fluent declaration of your validation logic to create your validation objects.

    If you could do it generically, to "automagically trigger" validation in your "CrudController" base class, or you'd have to do it on each of your controller endpoints, overriding the controller endpoint methods (they're virtual), I wouldn't be able to answer you without doing some more research into that particular library (Fluent Validator).

    However, I'd probably spend that time trying to figure it out, since if you could implement it generically on the CrudController base class, you'd avoid having to repeat your code, and you'd end up with much more DRY and beautiful code in the end.

    I imagine you could extend the CrudController base class with an (optional) "CustomValidator" instance, and if it's not null, invoke its "ValidateAndThrow" method. It doesn't seem to have any typing constraints, which makes it easier - The only remaining question is how you'd create an instance of your "CustomValidator", which I assume you could somehow accomplish using dependency injection, with (for instance) named resolvers or something similar ...?

    Notice, this would imply having to resist all sorts of shortcut temptations, such as making sure you're not ending up implementing the "Service Locator Anti Pattern", among other things - But if you spend some time really thinking it true, I am confident in that if you're able to understand its code, you'd be able to also pull this through ... :)

    Monday, June 10, 2019 4:34 AM
  • Psst, I just realised - Instantiate your "CustomValidator" in your specialised controller, and pass it into your "CrudController" base class :)
    Monday, June 10, 2019 4:39 AM
  • [Discard (almost) everything from me previous reply to you ...]

    I found your problem intriguing, and therefor I spent some additional neurons on it on my way to work :)

    Assuming the developers of Fluent Validator knows what they're doing, and they're thinking roughly the same way I do, they've implemented their library such that it creates a chain of compiled lambda expressions or something, which they do in the CTOR as you're invoking stuff such as "RuleFor". This implies there's a significant overhead creating one of these objects, which they've (hopefully) accommodated for by making sure a single instance of "AbstractValidator" is thread safe. (Notice, you should check up my assumptions)

    This would allow for you to reuse a single instance of "AbstractValidator" for every invocation to "ValidateAndThrow". Hence, you could create a new abstract template base class, and call it "CrudValidatorController", inheriting from "CrudController", where you accept an additional generic type, which you'd need to type constrain as "AbstractValidator<www.Model>" or something.

    This would allow you to override each virtual method where you need validation in your own inherited "CrudValidatorController", to invoke "AbstractValidator<www.Model>.ValidateAndThrow()", without caring about what particular specialised class it happens to be, and it would still work, and you'd pass in a reference to it during your invocation to the "CrudValidatorController"'s CTOR, that would be your "specialised type" (e.g. "TodoValidator", etc).

    This would allow you to inherit from "CrudValidatorController" instead of "CrudValidator", and then create your instance of your validator (e.g. "TodoValidator") as a static field in your specialised controller,passing it into your base class from your controller's CTOR, for then to have the "CrudValidatorController" actually perform the validation.

    This would result in keeping your code "Super DRY", avoiding duplication, and simply implying creating your validator type, add an instance to it as a static field to your specialised controller, and pass it into your "CrudValidatorController" base class CTOR.

    Monday, June 10, 2019 6:03 AM
  • Or to say it with a different language ... ;)

    public class CrudValidatorController<WebModel, DbModel, Validator> : CrudController<WebModel, DbModel> where Validator : AbstractValidator<WebModel>

    {

        readonly Validator _validator;

        public CrudValidatorController(ICrudService<DbModel> service, Validator validator)
          : base(service)
        {

            _validator = validator;

        }

        public override Guid Save([Required] [FromBody] WebModel model)

        {

            _validator?.ValidateAndThrow(model);

            base.Save(model);

        }

        /* ... etc ... */

    }

    For then to modify your specialized controller to something such as the following ...

    [Route("api/todos")]

    public class TodosController : CrudValidatorController<www.Todo, db.Todo, TodoValidator>

    {

        readonly static TodoValidator _validator = new TodoValidator();

        public TodosController(ITodoService service)

            : base(service, _validator)

        {}

    }

    For then to create your validator such as the following ...

    public class TodoValidator : AbstractValidator<www.Todo>

    {

        public TodoValidator()

        {

            RuleFor(x => /* ... validation logic here ... */);

        }

    }

    Assuming a single instance of AbstractValidator is actually thread safe, the above should do the trick ...

    Notice, I didn't actually try to compile this, but I think you get the idea :)



    Monday, June 10, 2019 6:28 AM
  • Totally agree about the WebModel being the optimum level to place the validation.

    In your second reply and subsequent code stub, I get where you are going with this.  The ability to step out and describe a new CrudValidatorController class based on CrudController where a Validator is accepted is slick!  That definitely provides a means for extension and I understand how this would then qualify as the super-dry way to do this by convention. I am gonna cobble some of this together so I can understand it better. Probably should do that before I shoot my mouth off. 

    Speaking of shooting my mouth off, I dug out an article I remember you wrote back in 2017 for Active Events.  I could barely grasp that concept but I totally understand the dependency enigma that OOP creates.  This architecture definitely shoots the middle finger at the classic way of doing things.  I can see why James gave you some grief about that one.  LOL.  He must've rolled his eyes when he saw Super-Dry draft!  

    I am curious about the lineage of Super-Dry.  Did it begin from Active Events?  Can you clue me in as to how the two architectures solve the same or different problems?  I am taking a swag here but I think Super-Dry and Magic would solve the dependency graph mess that Active Events solved for.  Is that the case or am I off out in the weeds here?  

    Anyway, keep up the good work!  The stuff you are conjuring up (pun intended) is compelling me to push my programming boundaries.  

    Thanks for your time, efforts & consideration. 


    Monday, July 22, 2019 3:40 AM
  • Hi Michael, I am currently in the process of actually merging these two different concepts these days together. It's too early to show you anything, but during a week or two, I will actually have transformed Magic into a "Super Signal" implementation, which is kind of a better version of Active Events (no reflection for instance).

    However, this time I am aiming much lower (pun), while at the same time implementing the scaffolding process described in this article - In the beginning for MySQL, later also for MS SQL. Basically, it'll allow you to point Magic at a database, click a button, and automagically have CRUD endpoints for every single table in that database.

    If you're too eager to wait, you can clone Magic in its current state, but it really needs a week or two more of work before it's even useful ...

    I'll keep the last release around though, which implements the ideas described in this article, as Release number 4. However, from the 4th version and onwards, it drastically changes, hopefully to the better I'd say :)

    As to the middle finger: Yup! I kind of tend to provoke that one flippin' up every now and then, due to my (extremely) non-conventional ways of looking at things ... ;)

    "This is your brain! This is your brain on OOP!" comes to mind :D

    Sorry for late answer, I didn't really see this one before now ...

    Wednesday, August 28, 2019 2:52 PM