6

Particularly for the sake of unit testing, I'm trying to implement an application with a layered architecture. I'm coding in C# and using ASP.NET Web API for the service layer. I'm aiming at a 3-layer architecture:

  1. Service Layer
  2. Business Logic Layer
  3. Data Access Layer

The Problem

I'm uncomfortable with having to choose between a relaxed architecture DAL or anemic BLL classes and I'm wondering whether it's because I'm approaching this the wrong way. When I refer to "relaxed architecture DAL", I mean a DAL that can be called by layers other than the BLL directly above it, i.e. the service layer.

My reason for not wanting a relaxed architecture DAL: makes it easy for business logic to be mistakenly bypassed.

My reason for not wanting anemic BLL classes: don't want a plethora of classes that do nothing other than pass calls between the service layer and DAL.

Example

Here's some sample code. First, a service layer method supporting the registration of a student in a class:

public class ClassRegistrationController : ApiController
{
    private readonly BusinessLogic.ClassRegistration _classRegistrationLogic;

    public ClassRegistrationController(
        BusinessLogic.ClassRegistration classRegistrationLogic)
    {
        _classRegistrationLogic = classRegistrationLogic;
    }

    [Route("/api/class/{classId}/register")]
    [HttpPost]
    public IHttpActionResult RegisterForClass(
        int classId,
        [FromBody]ClassRegistrationRequest registrationRequest)
    {
        try
        {
            _classRegistrationLogic.RegisterStudentForClass(
                classId,
                registrationRequest.StudentId);
        }
        catch (ClassFullException)
        {
            return BadRequest("Cannot register for class, it is full");
        }

        return Ok();
    }
}

And the business logic class supporting it:

public class ClassRegistration
{
    private DataLayer.IClassDataLayer _classDataLayer;
    private DataLayer.IClassRegistrationDataLayer _classRegistrationDataLayer;

    public ClassRegistration(
        DataLayer.IClassDataLayer classDataLayer,
        DataLayer.IClassRegistrationDataLayer classRegistrationDataLayer)
    {
        _classDataLayer = classDataLayer;
        _classRegistrationDataLayer = classRegistrationDataLayer;
    }

    public void RegisterStudentForClass(int classId, int studentId)
    {
        var classDetails = _classDataLayer.GetClassDetails(classId);

        if (classDetails.ClassIsFull)
        {
            throw new ClassFullException();
        }

        _classRegistrationDataLayer.AddStudentToClass(classId, studentId);
    }
}

The data layer interfaces (implementation isn't relevant to the question):

public interface IClassDataLayer
{
    Models.ClassDetails GetClassDetails(int classId);
}

public interface IClassRegistrationDataLayer
{
    int AddStudentToClass(int classId, int studentId);
}

Now, I think this all looks reasonable, and each class has a single (and not insignificant) responsibility. But, now let's say I want to add a service layer method that exposes details of a class, like:

[Route("/api/class/{classId}")]
[HttpGet]
public IHttpActionResult GetClassDetails(int classId)
{
    var classDetails = _classDetailsGetter.GetClassDetails(classId);

    if (classDetails == null)
    {
        return NotFound();
    }

    return Ok(classDetails);
}

The question is, will _classDetailsGetter be an instance of a class from the DAL or from the BLL?

If it's from the DAL, then I'm effectively saying it's "OK" for the service layer to go directly to the DAL, which opens up the risk of, say, somebody doing maintenance on the RegisterForClass service layer method deciding to go directly to IClassDataLayer instead of BusinessLogic.ClassRegistration.

But if it's from the BLL, then that BLL class would only be responsible for 1-to-1 call mapping, which also rubs me the wrong way. Like:

public class ClassDetailsLogic
{
    private readonly DataLayer.IClassDataLayer _classDataLayer;

    public ClassDetailsLogic(DataLayer.IClassDataLayer classDataLayer)
    {
        _classDataLayer = classDataLayer;
    }

    public Models.ClassDetails GetClassDetails(int classId)
    {
        return _classDataLayer.GetClassDetails(classId);
    }
}

Summary

Is there reason to favour the relaxed architecture DAL or the anemic BLL? Are my concerns regarding one (or the other) unfounded? Or is there something else altogether that I'm missing?

migrated from codereview.stackexchange.com Apr 14 '16 at 3:10

This question came from our site for peer programmer code reviews.

1

Data Transfer Objects (DTO's) have one purpose, and one purpose only: to retrieve data from a database and work with it in an object-oriented form.

There's no such thing as an "anemic BLL object." It's either a first-class BLL object, or it's a DTO. It's seldom both.

There are reasons why you have things like ViewModel objects. A View Model object contains data from your Model, but it's tailored to a View. It's not a DTO, at least not in the sense that you're transferring data back and forth from a database. It is an object in its own right, having its own specific purpose.

So where does that leave your Business Logic Layer?

The purpose of a Business Logic Layer is to convert business domain operations (such as Transfer Money or Build Widget) into CRUD operations. It doesn't actually perform the CRUD operations, though; that's what your DAL and DTO's do.

It seems like a lot of objects, doesn't it? But your DTO's are typically going to be generated by your Object-Relational Mapper, unless you plan on creating the database from your object domain (a perfectly valid technique).

Make use of partial classes.

So how do you marry your business logic code to your business objects? One way to do it is with partial classes. C# allows you to define your own class alongside the code-generated DTO, which will add your custom validation or whatever business logic you choose to add to the business domain object. Because your hand-written logic is in a separate class (having the same name), your code will be protected against being overwritten by the code-generated class, should you choose to re-generate your DTO's. Your DAL classes will no longer be anemic, because you will have added your own intelligence to them via partial classes.

  • The only DTO I can identify in my code is Models.ClassDetails. The class I'm concerned might be anemic is ClassDetailsLogic. I believe this fits your definition of a BLL, it's converting the domain operation of "get class details" into a cRud operation. That operation is a call to an instance of DataLayer.IClassDataLayer. But it's not adding anything to the logic. Calls to ClassDetailsLogic.GetClassDetails could just as easily go direct to IClassDataLayer.GetClassDetails. But skipping the BLL means a leaky DAL, and that makes me uncomfortable. – Snixtor Apr 14 '16 at 4:35
  • Why would you skip the BLL? – Robert Harvey Apr 14 '16 at 4:57
  • 1
    Presumably, the calls that go directly to the DAL were required when you originally designed the API. Refactoring might mean moving many of those direct calls behind a BLL method. So yes, your API is going to change if you do that. The alternative? Design a sensible API from the start that is business operation centric, one that doesn't require you to touch the database directly. Figure out what your business operations are going to be, and model those in your BL API instead. Or, y'know, live with the occasional direct DAL access. – Robert Harvey Apr 14 '16 at 5:34
  • 1
    Can you forecast ahead of time what all of those BLL methods are going to be? Is that an easy thing to do? Nope. Will your API change over time, as you discover new or previously undiscovered business problems that can be represented by a new BLL method? Almost certainly. The refactoring that you describe is probably inevitable as the application evolves. – Robert Harvey Apr 14 '16 at 5:36
  • 2
    By the way, the technique is apparently called "relaxed layer architecture." It's frequently employed in Onion architecture, where cross-cutting concerns are more prominent than they are in lasagna architectures. See methodsandtools.com/archive/onionsoftwarearchitecture.php – Robert Harvey Apr 14 '16 at 5:38
0

The two operations you are comparing cannot be compared. RegistrationStudentForClass is a commnand and GetClassDetails is a query. Commands and queries are very different in nature, that's why implementing your query as your command looks a bit off.

Commands implement a use case, they have validation, logic, rules, etc. Your controller delegate all this to the business logic and your business logic will use a repository or such to retrieve and then save the data models involved (ideally transactionally).

Queries on the other hand, don't have logic and don't have side effects. That's why you end up with nothing in your logic class. They are, as their name say, a query against your data store. The only "logic" they might have is making sure that the requester has access to the requested data. Some times you can do this in the controller, some times you need to parametrise your query. But there is no point in using the same structure or classes than your commands and I actually recommend against it.

You will find this approach under the name CQRS. Most of the times it's presented like a very complex pattern not recommended for small projects, but that's because some people think that CQRS implies using Event Sourcing, separation of read and write data stores, eventual consistency, etc. But if you take it only as the bare minimum definition of the pattern, so separating the stack for reads and for writes, but using the same data store, then it becomes very simple and it makes a lot of sense.

So, my advice is:

Do all your writes/commands as you expose, with your logic classes and repositories to load and save back the data.

Implement the queries completely separate from your commands. Do not reuse logic, models nor repositories. There are many ways of doing this, but a simple one is: Define a query interface like IClassroomQueries with the query methods that you need (don't let this grow too much, you don't want to break ISP too much). Each query method should expect a query dto object with the query parameters (classId for example) and it should return a result dto with exacly the fields that the consumer needs (no more, no less). If you have different consumers with different needs, define a different query method or even a different interface. Now feel free to implement every query the way that works best for each scenario. Some queries might use EF mapped directly to the DTOs. Others might map DTOs to a view. Others might use a stored procedure. Queries are not about consistency or code reuse. They are about efficiently returning the right data.

I will stop here, but I can follow up with some more implementation details if you are interested.

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.