[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 theEvent
object is mainly intended as syntactic sugar to allow clients to sayEvent.Builder
instead ofnew 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.
new Event { Property = "ABC" }
and use the usual model validation for other things. \$\endgroup\$