1

I am working on a web application that uses Angular 12 for the frontend and ASP.NET Core 6 for the backend. In our team we usually write just about anything as a 3-layer application (if possible) consisting of a presentation layer, a business logic layer and a data layer. In the case of this project, this would mean that when for example all entities of a certain type, let's call it Car, are requested from the server, the following would happen (simplified):

  1. The request arrives in the CarController.cs controller. The carManager instance here belongs to the business layer and provides functions for interacting with car entities (CRUD for the most part). Dependency Injection is used to inject the carManager instance into the controller:

    public async Task<ActionResult<List<Car>>> GetCars()
    {
        List<Car> cars = await this.carManager.GetCarsAsync();
        return this.Ok(cars);
    }
    
  2. If there is anything business logic related, it will be done in the GetCarsAsync() function of the carManager. However, in many cases this is not necessary. Therefore, I often end up with "dumb" functions that just call the corresponding function of the data layer like this. The carAccessor in this example belongs to the data layer. Dependency Injection is also used here to inject its instance into the manager:

    public async Task<ActionResult<List<Car>>> GetCarsAsync()
    {
        return this.carAccessor.GetCarsAsync();
    }
    
  3. The actual querying of the data is then done in the corresponding accessor for the entity. Example:

    public async Task<List<Car>> GetCarsAsync()
    {
        List<Car> cars = new();
        List<TblCar> dbCars = await this.Context.TblCars.ToListAsync();
    
        foreach (TblCar dbCar in dbCars)
        {
            Car car = <AutoMapper instance>.Map<Car>(dbCar);
            cars.Add(car);
        }
    
        return cars;
    }
    

While this makes it easy to not mix up business logic and data access in the application, it really creates a whole lot of code that does nothing than just call other code. Therefore, in some cases the business layer is omitted simply because of laziness and business logic ends up just being implemented somewhere else, for example directly in the controller. Is there a simple solution for this?

Chris
  • 1,417
  • 4
  • 21
  • 53
  • I'd argue that the data layer returning mapped entities is completely wrong, but that's opinionated. I don't know what you expect us to suggest here though – Camilo Terevinto Jan 10 '22 at 09:02
  • @CamiloTerevinto why do you think the data layer is completely wrong? What kind of approach would you use instead? – NoConnection Jan 10 '22 at 09:06
  • 1
    @NoConnection Because the data layer is supposed to be for getting and storing data, not mapping it to the likes of a UI. What do you do when you need to modify the entities or work with related entities? Do multiple separate queries? That almost entirely throws away the reasoning for using a full ORM like EF. That said, for extremely simple CRUD apps like this, I wouldn't have a separate data layer (in fact, I have not used a 3-layer architecture in ages) – Camilo Terevinto Jan 10 '22 at 09:09
  • @CamiloTerevinto It was done like this in almost any project I've seen so far. Otherwise we'd have to deal with all sorts of data source specific things in the business layer. Especially when having multiple data sources (which we actually have in this project) it would mean that we would have to work with EF-generated classes together with data from other data sources (which are sometimes literally just XML or JSON strings). Mapping the data like this allows other layers to work with the data without having to care about data source specific things. What would you suggest doing instead? – Chris Jan 10 '22 at 10:05
  • @CamiloTerevinto Btw, the main thing I am looking for here is anything that helps to reduce the amount of "dumb" code that really contains no actual logic while still keeping everything in the layer it belongs to. – Chris Jan 10 '22 at 10:50

1 Answers1

0

Using generic base classes and inheritance could help you to reduce writing and maintaining of similar code everywhere for vanilla cases :

public abstract class BaseController<T, C>: Controller  where T: class where C: IItemsManager<T>
{
    protected C ItemManager { get; }

    public virtual async Task<ActionResult<List<T>>> GetCars()
    {
        List<T> items = await this.ItemManager.GetItemsAsync();
        return this.Ok(items);
    }
}

public interface IItemsManager<T> where T : class
{
    Task<List<T>> GetItemsAsync();
}


public class ItemsManager<T, B>: IItemsManager<T> 
    where T: class 
    where B: class
{
    public virtual async Task<List<T>> GetItemsAsync()
    {
        List<T> items = new List<T>();
        List<B> dbItems = await this.Context.Set<B>().ToListAsync();

        foreach (B dbItem in dbItems)
        {
            T item = <AutoMapperinstance>.Map<T>(dbItem);
            items.Add(item);
        }

        return items;
    }
}

public class CarManager: ItemsManager<Car, TblCar>
{

}

public class CarController: BaseController<Car, CarManager>
{

}
Dubbs777
  • 347
  • 2
  • 7
  • I've been giving this approach a try but at the end I always run into compiler error CS0311 in line `public class CarController: BaseController`. The error says that I cannot use `CarManager` as `C` in the generic type `BaseController` because there is no implicit reference conversion from `CarManager` to `ItemsManager`. Any idea how to fix that? – Chris Jan 10 '22 at 14:23
  • @Chris, It compiles on my side. Could you check your code ? the base controller has the where T: class where C: IItemsManager constraint and as long as C implements IItemsManager, you should be OK. – Dubbs777 Jan 12 '22 at 08:25
  • PS : I edited and made some minor changes (removed the unallowed async in interface for instance) but they should not be related to your issue I guess. – Dubbs777 Jan 12 '22 at 08:31