Hannah Hayes
Hannah Hayes

Reputation: 325

Is the service layer in my .NET Core/Blazor architecture redundant?

I have a data access layer (a C# library), a .NET Core application that I intend to use as a service and several projects that need to talk to this service, including a Blazor WASM project.

My DAL has a DbContext that looks like this:

public class QualityAssuranceDbContext : DbContext
{
    public QualityAssuranceDbContext(DbContextOptions<QualityAssuranceDbContext> options)
        : base(options)
    {
    }

    public QualityAssuranceDbContext()
    {
    }

    public DbSet<Aspect> Aspects { get; set; }

    //PLUS OTHER ENTITIES REDACTED FOR BREVITY
}

In my .NET Core service layer, I have a BaseRepository, a UnitOfWork and a service class that currently looks like this:

public class QualityAssuranceService : IQualityAssuranceService
    {
        private readonly IUnitOfWorkQA _unitOfWork;
        private static HttpContext _httpContext => new HttpContextAccessor().HttpContext;

        public QualityAssuranceService(IUnitOfWorkQA unitOfWork)
        {
            _unitOfWork = unitOfWork;
        }

        public async Task<IEnumerable<AspectDto>> GetAspects(int schoolId, bool includeDeleted = false)
        {
            var aspects = await _unitOfWork.Aspects.GetAll(
                expression: q =>
                    q.SchoolId == schoolId &&
                    q.Enabled == true,
                orderBy: o => o
                    .OrderBy(x => x.Name));

            var aspectsDto = (from aspect in aspects
                              select new AspectDto()
                              {
                                  Id = aspect.Id,
                                  Name = aspect.Name,
                                  SchoolId = aspect.SchoolId,
                              }).ToList();

            return aspectsDto;
        }

        //PLUS OTHER ACTIONS REDACTED FOR BREVITY
        
        public void Dispose()
        {
            _unitOfWork.Dispose();
            GC.SuppressFinalize(this);
        }
    }

Finally, in my Blazor app I have a Program.cs file in my server project that looks like this:

public partial class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            //REDACTED AS IRRELEVANT TO QUESTION

            var QualityAssuranceConnectionString = builder.Configuration.GetConnectionString("QualityAssuranceConnection");
            builder.Services.AddDbContext<QualityAssuranceDbContext>(options =>
            {
                options.UseSqlServer(QualityAssuranceConnectionString);
                options.EnableSensitiveDataLogging(true);
            });
            builder.Services.AddTransient<IUnitOfWorkQA, UnitOfWorkQA>();
            builder.Services.AddTransient<IQualityAssuranceService, QualityAssuranceService>();
            
            var app = builder.Build();

            //REDACTED AS IRRELEVANT TO QUESTION

            app.Run();
        }
    } 

and a controller that injects the service like this:

public class QualityAssuranceController : ControllerBase
    {
        private readonly IQualityAssuranceService _qualityAssuranceService;

        public QualityAssuranceController(IQualityAssuranceService qualityAssuranceService)
        {
            _qualityAssuranceService = qualityAssuranceService;
        }

        [HttpGet]
        public async Task<IActionResult> GetAspects(bool includeDeleted = false)
        {
            var schoolId = GetSchoolId();

            var aspectDtos = await _qualityAssuranceService.GetAspects(schoolId, includeDeleted);

            return Ok(aspectDtos);
        }
    }

My question is: is the .NET Core service app redudant in this scenario? The reason I ask is because I originally intended for the .NET Core service app to contain the connection string for the database and to register the DbContext in its Program.cs file. However, I quickly realised that I couldn't inject the service into my Blazor controller without registering the UnitOfWork and DbContext in my Blazor's Program.cs file.

The service will be referenced by both my Blazor app and another .NET Core app so it's useful in that it prevents us from having to replicate some of the code, but I thought the purpose of that layer was also to add a level of abstraction between my Blazor app and the database - surely that's not what's happening here if the connection to the database is in my Blazor app?

I should add that at the moment, this all works in that I am able to connect to the database from my Blazor app and perform basic CRUD commands without any issue.

Edit:

I tried getting my service to work in my controller using IServiceScopeFactory like so:

private readonly IServiceScopeFactory _serviceScopeFactory;

        public QualityAssuranceController(IServiceScopeFactory serviceScopeFactory)
        {
            _serviceScopeFactory = serviceScopeFactory;
        }

        [HttpGet]
        public async Task<IActionResult> GetAspects(bool includeDeleted = false)
        {
            var schoolId = GetSchoolId();

            using (IServiceScope scope = _serviceScopeFactory.CreateScope())
            {

                IQualityAssuranceService qualityAssuranceService = scope.ServiceProvider.GetRequiredService<IQualityAssuranceService>();
                
                var aspectDtos = await qualityAssuranceService.GetAspects(schoolId, includeDeleted);
                
                return Ok(aspectDtos);
            } 
        }

and I removed the following from my Blazor Program.cs file:

var QualityAssuranceConnectionString = builder.Configuration.GetConnectionString("QualityAssuranceConnection");
            builder.Services.AddDbContext<QualityAssuranceDbContext>(options =>
            {
                options.UseSqlServer(QualityAssuranceConnectionString);
                options.EnableSensitiveDataLogging(true);
            });
            builder.Services.AddTransient<IUnitOfWorkQA, UnitOfWorkQA>();
            builder.Services.AddTransient<IQualityAssuranceService, QualityAssuranceService>();

But I'm just getting an error to say that IQualityAssuranceService isn't registered.

Edit 2:

The basic structure of my solution looks like this:

Project 1: DAL - C# library with multiple DbContexts (our app has separate modules so we have the data for each module in a separate database)

Project 2: DTOs - C# library with data transfer objects used by both Blazor app and .NET Core service app

Project 3: Models - C# library with all the models that the DbContexts reference

Project 4: Services.QualityAssurance - .NET Core app with a single service class that performs CRUD operations on the database. Ideally the connection string sits and is used from here.

Project 5a: Blazor Client - front end of Blazor WASM

Project 5b: Blazor Server - back end of Blazor WASM. This has a controller which I need to get talking to the service class in Services.Quality Assurance. At the moment I can only get this working if I register the DbContext and QualityAssuranceService in the Program.cs file. This (I think) means the connection string also needs to be in the Blazor Server project.

Upvotes: 2

Views: 1580

Answers (3)

Roberto Ferraris
Roberto Ferraris

Reputation: 1119

I've a solution that is strucured in a similar way, except that the ASP.NET Host Server also act as the Rest API endpoint. But logically the two functionality are separated.

I found the use of Blazor Server (as you call the ASP.NET Core App that host Blazor WASM) a good way to manage (deploy and debug) the Blazor application, and I found some suggestion to use it in some authentication scenarios (e.g. IdentityServer documentation suggest to use it).

But I don't think it's a good choice to use it as a gateway to the API service. Simply let the Blazor WASM app to comunicate with the Rest API server. You remove a level of indirection and reduce the latency in comunication. Probably you must configure some additional authorization stuff, but if you have other apps that access the API you already do it.

Personally I choosed to embed both API and blazor host in the same ASP.NET Core application because it simplify the deploy, in fact I could deploy as a single serverless WebApp on azure.

The use of a API gateway could be a good choice in case of multiple micro-services that you want to expose as a single endpoint. In this case I suggest to follow patterns described in .NET Microservices: Architecture for Containerized .NET Applications - from MS Learn.

Upvotes: 0

Varin
Varin

Reputation: 2453

I have an MVC app with a complex Calendar component that I built. Had similar issue, as I already have a service layer with dbcontext injected into each service and wanted to reuse it rather than using dbContext directly or doing some weird stuff with HTTPClient in Blazor Server.

The solution for me was using:

[Inject] private IServiceScopeFactory ServiceScopeFactory { get; set; }

private async Task OnSubmit()
    {

        using (IServiceScope scope = ServiceScopeFactory.CreateScope())
        {

            ITenantService tenantService = scope.ServiceProvider.GetRequiredService<ITenantService>();
            IUserService userService = scope.ServiceProvider.GetRequiredService<IUserService>();
            IAppointmentService appointmentService = scope.ServiceProvider.GetRequiredService<IAppointmentService>();
            ITimesheetService timesheetService = scope.ServiceProvider.GetRequiredService<ITimesheetService>();

            // all logic using services

        }

}

This way I create a separate scope, fetch the services. Going to DI directly didn't work for me as SignalR didn't like the scope of my servives/dbcontext.

In my MVC app all services are scoped and sit in the container, and dbContext is injected into all of those services constructors. So When I create the scope in my Blazor component to do some stuff, the services have all they need to perform all db operations.

Upvotes: 0

MrC aka Shaun Curtis
MrC aka Shaun Curtis

Reputation: 30310

In a design you normally think in three basic layers:

  1. Infrastructure: about how you get and persist data.
  2. Core/Business: where your business logic and objects reside.
  3. Presentation: your interface with UI.

The basic relationship between these is this:

Presentation => Core/Business <= Infratructure

Core depends on no one. Presentation has no knowledge of infrastructure objects and visa versa.

Services exist at all layers.

The Core/Business domain defines it's contracts with the Infrastructure domain through interfaces. The Infrastructure domain provides the implementations of these interfaces. The Presentation domain can call directly into your Core/Business domain, so there's no real need to define interfaces.

In your case consider the Controller the presentation layer: it presents data to the outside world. Your Service layer is the Core domain and your DAL is Infrastructure.

Entity Framework implements both Unit of Work and Repository patterns so there's an obvious route to shrink the layers. The primary reasons not are:

  1. Unit Testing.
  2. Keep objects small, focused and single responsibility.
  3. It breaks a core tenet of clean design: a direct link between the presentation and infrastructure domains.

Think of the core and infrastructure layers as the data pipeline. They provide data to a Blazor Server application, an SSR application, a Server API application,...

Blazor, like most SPA's and modern applications is designed to run in an asynchronous world. Most modern data stores and Http clients are also async.

You need to take this into consideration in your Infrastructure domain code. The solution with databases is to use DbContext factory and for API calls a HttpContext factory. Using a DbContext factory makes a unit of work implementation simple.

There's an interesting discussion on the topic here - https://garywoodfine.com/generic-repository-pattern-net-core/

Upvotes: 1

Related Questions