1
\$\begingroup\$

[Posted yesterday on Software Engineering, but was apparently "disappeared"...maybe better here]

Background: I am just starting to get my head around the idea of separating the domain model from the persistence layer (per Uncle Bob), service layer/IoC/separation of concerns/dependency injection, looking at some aspects of DDD (possibly aggregates?), enforcing invariants (e.g. having all private setters below)... And I'm trying to come up with some patterns that assist with those goals — ones the rest of the team can follow and adapt... I'm making multiple leaps in understanding and don't want to twist my ankle when I land.

One result of my effort is this builder pattern for large objects (lots of fields), which implements a fluent API and tries to allow for subclassing of the Builder/target pair allowing builder superclass reuse (THAT required some generics gymnastics). My thought is for a subclass of the domain model object to become a EF Core entity with persistence-specific properties and methods in the subclass, but core business logic still able to exist (and be tested) on the core domain model objects.

Some notes:

  • Classes and business logic are clearly incomplete.
  • The nested classes are used because they allow for private/protected member access by the Builder.
  • Assume that Id is a store-specific property and not needed for other use cases.
  • The Builder property on the Event object is mainly intended as syntactic sugar to allow clients to say Event.Builder instead of new Event.EventBuilder<Event>().
  • And if anyone wonders, I'm not going to wrap all of EF Core in a repository/UoW pattern (for now I'm buying the arguments that DbContext is a UoW and DbSet is a repository)...though I am going to put a repository wrapper around some particular entities (like audit events) that I may want to implement with a different store type.

So, here's a base Event class/domain model class with an inner EventBuilder<T> class:

using System;
using System.Collections.Generic;

namespace App.Core
{
    public class Event
    {
        public String Title { get; private set; }
        public DateTimeOffset Start { get; private set; }
        public DateTimeOffset End { get; private set; }
        // Lots more properties omitted here...
        protected Event() { }

        public static EventBuilder<Event> Builder => new Event.EventBuilder<Event>();
        public class EventBuilder<T> where T : Event
        {
            protected virtual T Result { get; set; } = Activator.CreateInstance(typeof(T), true) as T;
            public EventBuilder<T> WithTitle(String title)
            {
                Result.Title = title;
                return this;
            }

            public EventBuilder<T> WithStartAndEnd(DateTimeOffset start, DateTimeOffset end)
            {
                if (start == null)
                {
                    throw new ArgumentNullException(nameof(start));
                }
                if (end == null)
                {
                    throw new ArgumentNullException(nameof(start));
                }
                if (end < start)
                {
                    throw new ArgumentOutOfRangeException(nameof(end), end, "End date must be on or after the start date.");
                }
                this.Result.Start = start;
                this.Result.End = end;
                return this;
            }
            public virtual T Create()
            {
                // Last chance validation here!
                return this.Result; 
            }
        }
    }
}

And here's a subclass with persistence-specific features:

namespace App.Repository.Entities
{
    public class Event : App.Core.Event
    {
        public int Id { get; private set; }

        public new static EventBuilder<Event> Builder => new Event.EventBuilder<Event>();

        public new class EventBuilder<T> : App.Core.Event.EventBuilder<T> where T : Event
        {
            //protected new T _result = Activator.CreateInstance<T>();
            //protected new T _result = Activator.CreateInstance(typeof(T), true) as T;
            public EventBuilder<T> WithId(int id)
            {
                this.Result.Id = id;
                return this;
            }

            public override T Create()
            {
                // Last chance validation for subclass here!
                // This is safe because we know it's operating on a subclass this.Result:
                var result = base.Create() as T;
                return result;
            }
        }
    }
}

And here's some naive client code that uses the subclass:

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using App.Repository.Entities; // <-- Note this namespace

namespace App.Web.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class EventsController : ControllerBase
    {
        // GET: api/Event
        [HttpGet]
        public ActionResult<IEnumerable<Event>> Get()
        {
            return new Event[] {
                Repository.Entities.Event.Builder
                    .WithId(5126)
                    .WithTitle("The Apocalypse")
                    .WithStartAndEnd(
                        new DateTimeOffset(2012, 12, 21, 6, 12, 0, new TimeSpan(-4,0,0)), 
                        new DateTimeOffset(2012, 12, 21, 6, 12, 1, new TimeSpan(-4,0,0))
                        )
                    .Create()
            };
        }
        // Other REST methods omitted...
    }
}

So, do you see anything glaringly stupid about the above that is going to fry me when I try to actually use it? There is certainly some naivety in the above and I would appreciate other comments/suggestions, but a good answer will point out a flaw that will make me want to substantially change the design or throw it out altogether.

\$\endgroup\$
6
  • 2
    \$\begingroup\$ One question... what is the benefit of doing this? Usually a builder simplifies building complex objects or makes raw APIs convenient but your solution isn't providing any of that. Since you are using this for dtos, I find it's much easier to just say new Event { Property = "ABC" } and use the usual model validation for other things. \$\endgroup\$
    – t3chb0t
    Commented Aug 23, 2019 at 13:04
  • \$\begingroup\$ Interesting subject: I found 2 similar questions, not that this is in any way a duplicate: codereview.stackexchange.com/questions/133066/… and codereview.stackexchange.com/questions/151078/… \$\endgroup\$
    – dfhwze
    Commented Aug 23, 2019 at 13:37
  • 1
    \$\begingroup\$ @dfhwze there is one more experiment. Have you seen this insane idea too? codereview.stackexchange.com/questions/210056/… :-P \$\endgroup\$
    – t3chb0t
    Commented Aug 23, 2019 at 13:47
  • 1
    \$\begingroup\$ @t3chb0t that one is even for your standards out of the ordinary o_0 \$\endgroup\$
    – dfhwze
    Commented Aug 23, 2019 at 13:50
  • 3
    \$\begingroup\$ You have to post the complete code so that we have the full picture of your work. Until this is done, I vote to close this question as lacking context. Let's continue this topic when the question is updated. \$\endgroup\$
    – t3chb0t
    Commented Aug 23, 2019 at 14:37

1 Answer 1

3
\$\begingroup\$

I'm not sure this qualifies as a review, but here we go anyways.

Don't do this. This is one of the classic use of a design pattern where we shouldn't use one. A design pattern is a solution to a known problem with set boundaries and a very specific scenario. The builder is used to build complex objects in a flexible manner. A DTO isn't a complex object and shouldn't be built in a flexible manner. If your DTOs are complex, that is the problem you should tackle with your team, not trying to find a work to work with complex DTOs.

When reading your first paragraph, I swear I could've written it 5 years ago. I've written a generic equality checker (in hopes that it would help make equality comparison easier, where it's already really easy. What happened is :

  1. We found edge cases that weren't supported, ended up working on it a lot more to fix the edge cases
  2. It got complicated and new developers had a hard time figuring out how it worked
  3. It died and is probably to this date still marked as [Deprecated] in a code base

Now, I ask myself three questions before I try to add a "generic" tool that'll "help" my fellow developers :

  • Is the problem I'm trying to solve complex enough to require such a solution?
  • Am I 100% sure that what I've written won't explode in a mess of edge cases that'll cost more to maintain than not having the solution at all?
  • Will other developers pick it up easily and understand how it works?

I'd say that 9 times out of 10, the answer is no to at least one of those questions and I end up not doing it. It takes experience to know when not to do something, because as developers we always want to work with the cool stuff, but most of the time it creates more complexity than it solves.

Now, that doesn't mean you should fiddle with ideas like these, because you can learn a lot about many things (reflection, expressions, generics, etc.). But you need to develop the reflex to ask yourself "Should this be used in a production code that'll need to be maintained for years, potentially without me?" and think hard about the possible consequences of using whatever you're going to build.


The new keyword is probably the biggest "maintainability danger" of the C# language (In my opinion). I really think you should use whatever other way you have not to use it (ie. removing the nested class). Use interfaces, dependency injection, etc. not to use the new keyword. (I'm obviously not talking about class instantiation)

\$\endgroup\$
11
  • \$\begingroup\$ Well, OP has just admitted that their code is heavily shortened so I think any review doesn't make much sense as we don't see the real complexity and the problem they're facing. \$\endgroup\$
    – t3chb0t
    Commented Aug 23, 2019 at 14:41
  • 1
    \$\begingroup\$ @t3chb0t Well that sucks because I took time to write my answer ahah, but I'm almost certain that whatever is added to the code won't make this builder pattern worth it. \$\endgroup\$
    – IEatBagels
    Commented Aug 23, 2019 at 14:43
  • \$\begingroup\$ True, your answer is so universal that whatever comes, it'll stay valid, \$\endgroup\$
    – t3chb0t
    Commented Aug 23, 2019 at 14:44
  • \$\begingroup\$ Thanks for this. Question... both you and @t3chb0t have referred to me using this pattern to make DTOs... But I don't see it that way, exactly... I'm planning to use this for model objects (primarily), but my pattern is to have the DTOs be a subclass of the model object, with the persistence dependencies. Is this fundamentally flawed? If (and this is not guaranteed of course), my model object's properties closely match what I want in my persistent model, maybe plus some housekeeping properties/methods? \$\endgroup\$
    – S'pht'Kr
    Commented Aug 23, 2019 at 14:47
  • \$\begingroup\$ I should add that I'm coming from using EF4 + MVC with pretty much all my business logic in the controllers... and trying to move domain logic into domain model objects and application logic into a service layer. This DTO-as-subclass model made sense to me if I could make it work, and this pattern was one part of getting there. Keeping the setters private is done to help me control state via behavioral model methods, hence I had to think about initializing an object with many properties (domain model or DTO, either way). \$\endgroup\$
    – S'pht'Kr
    Commented Aug 23, 2019 at 14:50

Not the answer you're looking for? Browse other questions tagged or ask your own question.