3
\$\begingroup\$

I have a project template and like to usually work with the following setup (grossly simplified, but that's the gist of it).

CompanyName.ProjectName.Business.Entities

I define my business objects. I am not going after DDD, so having [Key] is definitely not an issue for me. Nevertheless, the classes are true business objects and they have as much business logic rules in them as necessary.

The following is an over-simplified example of such object, with the only business logic in it - "description is required".

public class BugReport
{
    [Key]
    public int ID { get; private set;}

    public string Description { get; private set; }

    public BugReport(string description)
    {
        if (string.IsNullOrWhiteSpace(description))
        {
            throw new ArgumentNullException("description");
        }

        this.Description = description;
    }

    protected BugReport()
    {
        // Required by EF
    }
}

CompanyName.ProjectName.Business.Interface.DataRepository

Although my solution is tightly coupled with Entity Framework and I don't make an effort to abstract this (it is pointless), I don't want anything working directly with DbContext, so I put an interface on top of it, to expose only what I want the consumers of the 'data access' to see.

public interface IDataRepository : IDisposable
{
    IDbSet<BugReport> BugReports { get; set; }
}

public interface IDataRepositoryManager : IDataRepository
{
    void Save();
}

In CompanyName.ProjectName.Infrastructure.EfRepository

public class EfDataRepository : DbContext, IDataRepositoryManager
{
    public IDbSet<BugReport> BugReports { get; set; }

    public EfDataRepository() : base("dataRepositoryConnection")
    {
    }

    public void Save()
    {
        this.SaveChanges();
    }
}

CompanyName.ProjectName.UI.MVC

What I like most about this architecture is that I can just do this:

public class HomeController : Controller
{
    private DataRepositoryFactory<IDataRepository> dataRepositoryFactory;

    public HomeController(DataRepositoryFactory<IDataRepository> dataRepositoryFactory)
    {
        if (dataRepositoryFactory == null)
        {
            throw new ArgumentNullException("dataRepositoryFactory");
        }

        this.dataRepositoryFactory = dataRepositoryFactory;
    }

    public ActionResult ListHighPrioBugs()
    {
        using (var dataRepository = this.dataRepositoryFactory.Create())
        {
            var someBugs = dataRepository.BugReports.Where(bug => bug.Priority > 100);

            // Build model, show view ...
        }
    }
}

and let the UI query whatever data it needs, in order to build the UI. I don't have to create 'generic repository' (yuck!), I don't have to provide a method GetBugsWithPrioOver(int prio) (double-yuck!), etc. I let the UI do their own queries.

CompanyName.ProjectName.Business.Services

Now, let's say that when a new bug is reported, I have to send some kind of a notification. Then, I would create a 'domain service', such as:

public class BugReportsService : CompanyName.ProjectName.Business.Interfaces.BusinessServices.IBugReportsService
{
    private DataRepositoryFactory<IDataRepositoryManager> dataRepositoryManagerFactory;
    private INotificationService notificationService;

    public BugReportsService(DataRepositoryFactory<IDataRepositoryManager> dataRepositoryManagerFactory, INotificationService notificationService)
    {
        if (dataRepositoryManagerFactory == null)
        {
            throw new ArgumentException("dataRepositoryFactory");
        }

        if (notificationService == null)
        {
            throw new ArgumentException("notificationService");
        }

        this.dataRepositoryManagerFactory = dataRepositoryManagerFactory;
        this.notificationService = notificationService;
    }

    public void CreateNewBug(BugReport bugReport)
    {
        if (bugReport == null)
        {
            throw new ArgumentException("bugReport");
        }

        using (var dataRepositoryManager = this.dataRepositoryManagerFactory.Create())
        {
            dataRepositoryManager.BugReports.Add(bugReport);
            dataRepositoryManager.Save();
        }

        this.notificationService.NotifyAbout(bugReport);
    }
}

Then, my controller would look like:

public class CreateBugController : Controller
{
    private BugReportsService bugReportsService;

    public CreateBugController(BugReportsService bugReportsService)
    {
        if (bugReportsService == null)
        {
            throw new ArgumentNullException("bugReportsService");
        }

        this.bugReportsService = bugReportsService;
    }

    public ActionResult CreateBug(NewBugModel model)
    {
        var bugReport = MapFrom(model);
        this.bugReportsService.CreateNewBug(bugReport);
    }
}

Generally, I don't create a domain service for an operation which can easily be done in the UI controller, using the data repo to get the object, invoke a method on it and save. I do Domain Services only when the business logic is more complex, involves the state of more than one object and generally should not be implemented in the UI, because it is considered business logic. Right?

Now, I am generally very happy with this approach. The only thing that seems fishy to me is that in the UI someone could do:

public class CreateBugController : Controller
{
    private DataRepositoryFactory<IDataRepositoryManager> dataRepositoryManagerFactory;

    public CreateBugController(DataRepositoryFactory<IDataRepositoryManager> dataRepositoryManagerFactory)
    {
        if (dataRepositoryManagerFactory == null)
        {
            throw new ArgumentNullException("dataRepositoryManagerFactory");
        }

        this.dataRepositoryManagerFactory = dataRepositoryManagerFactory;
    }

    public ActionResult CreateBug(NewBugModel model)
    {
        var bugReport = MapFrom(model);

        // Warn: The following is kind of a problem. The business rule is 'when a new bug is created, send notification'. Here the UI developer is free to directly insert the bug record into the data repo, without firing the notification event, provided by the BugReportsService.
        using (var dataRepositoryManager = this.dataRepositoryManagerFactory.Create())
        {
            dataRepositoryManager.BugReports.Add(bugReport);
            dataRepositoryManager.Save();
        }
    }
}

Same thing for the delete. I am mostly worried about Remove and Add, directly on the repository, when there should have been used a domain service. All the interface segregation in the world wouldn't help me prevent a developer from doing this. How worried should I be about this?

Basically, my biggest concern is the UI developers doing something directly on the objects/repository, which should have been done through a domain service. On the other hand, the business objects protect themselves, by having private setters and a lot of validation in the methods which modify the properties. The object cannot be left in a wrong state.

Overall, what do you think about this architecture? Feel free to comment on any of the components.

\$\endgroup\$
3
\$\begingroup\$

If you are exposing the dbset and allowing the clients (users of the class) to write queries, there will be queries all over the place. If you decide at some point to replace a certain operation with a stored procedure instead, for performance reasons, you have to find those queries everywhere. Or if you introduce a soft delete column for a certain table, you have to find all queries to exclude those records. I wouldn't expose dbsets.

If you don't want the presentation tier to go right against the repository but through another layer, you can create a layered diagram in Visual Studio and enforce which layer can talk to which as shown in https://msdn.microsoft.com/en-us/library/dd409395.aspx During compilation it will generate errors if the presentation layer is referencing the repository layer.

\$\endgroup\$
  • \$\begingroup\$ Thanks for your reply. I don't foresee introducing stored procedures anywhere in the near future. "Or if you introduce a soft delete column for a certain table, you have to find all queries to exclude those records" - No, I could just change the original IDbSet be a filtered set and re-introduce the original unfiltered one as a new property on the Repo. \$\endgroup\$ – Hristo Yankov Nov 27 '16 at 7:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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