DEV Community

Cover image for Build a To-Do app - Part 2 - .NET API
Alexandro Martinez
Alexandro Martinez

Posted on

Build a To-Do app - Part 2 - .NET API

We'll create the Model, DTOs, Repository and the Controller. Then run the database migration and test the CRUD app.

In this part, we'll implement our .NET API to persist the tasks.

Requirements


Steps

  1. Domain Model
  2. Application DTOs
  3. Task Repository
  4. Controller
  5. Migrations
  6. Run the Application

1. Domain Model

On the Domain Layer (NetcoreSaas.Domain.csproj), add:

  • Enums/Modules/Todo/TaskPriority.cs
  • Models/Modules/Todo/Task.cs

1.1. TaskPriority.cs enum

src/NetcoreSaas.Domain/Enums/Modules/Todo/TaskPriority.cs

namespace NetcoreSaas.Domain.Enums.Modules.Todo
{
    public enum TaskPriority
    {
        Low,
        Medium,
        High
    }
}
Enter fullscreen mode Exit fullscreen mode

1.2. Task Model

Naming our model class as Task could be a problem since could interfere with the existing System.Threading.Tasks.Task class, but we'll deal with that.

src/NetcoreSaas.Domain/Models/Modules/Todo/Task.cs

using NetcoreSaas.Domain.Enums.Modules.Todo;
using NetcoreSaas.Domain.Models.Core;

namespace NetcoreSaas.Domain.Models.Modules.Todo
{
    public class Task: AppWorkspaceEntity
    {
        public string Name { get; set; }
        public TaskPriority Priority { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Application DTOs

According to Martin Fowler: the Service Layer defines the application's boundery, it encapsulates the domain. In other words it protects the domain.

-- StackOverflow Answer

On the Application Layer (NetcoreSaas.Application.csproj) we'll touch the following files:

  • (Create) Dtos/Modules/Todo/TaskDto.cs
  • (Update) Mapper/MappingProfile.cs
  • (Create) Contracts/Modules/Todo/CreateTaskRequest.cs
  • (Create) Contracts/Modules/Todo/UpdateTaskRequest.cs

2.1. TaskDto.cs

src/NetcoreSaas.Application/Dtos/Modules/Todo/TaskDto.cs

using NetcoreSaas.Application.Dtos.Core;
using NetcoreSaas.Domain.Enums.Modules.Todo;

namespace NetcoreSaas.Application.Dtos.Modules.Todo
{
    public class TaskDto: AppWorkspaceEntityDto
    {
        public string Name { get; set; }
        public TaskPriority Priority { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode

2.2. Mapper Configuration

src/NetcoreSaas.Application/Mapper/MappingProfile.cs

...
+ using NetcoreSaas.Application.Dtos.Modules.Todo;
+ using NetcoreSaas.Domain.Models.Modules.Todo;

namespace NetcoreSaas.Application.Mapper
{
    public class MappingProfile : Profile
    {
        public MappingProfile()
        {
             ...
+            CreateMap<Task, TaskDto>();
+            CreateMap<TaskDto, Task>();
        }
        ...
Enter fullscreen mode Exit fullscreen mode

2.3. CreateTaskRequest.cs contract

src/NetcoreSaas.Application/Contracts/Modules/Todo/CreateTaskRequest.cs

using NetcoreSaas.Domain.Enums.Modules.Todo;

namespace NetcoreSaas.Application.Contracts.Modules.Todo
{
    public class CreateTaskRequest
    {
        public string Name { get; set; }
        public TaskPriority Priority { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode

2.4. UpdateTaskRequest.cs contract

src/NetcoreSaas.Application/Contracts/Modules/Todo/UpdateTaskRequest.cs

using NetcoreSaas.Domain.Enums.Modules.Todo;

namespace NetcoreSaas.Application.Contracts.Modules.Todo
{
    public class UpdateTaskRequest
    {
        public string Name { get; set; }
        public TaskPriority Priority { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Task Repository

If you're just getting started on the Repository Pattern, I recommend you to read Mosh Hamedani content.

On our Application Layer we define Repository, and we implement them on our Infrastructure Layer.

We'll add the Repository to our Unit of Work instances and perform CRUD operations. There are 2 options:

  • IMasterUnitOfWork (uses IMasterDbContext): All records
  • IAppUnitOfWork (uses IAppDbContext): Filters current Tenant and Workspace

For our Task CRUD we need IAppUnitOfWork instance.

3.1. Interface

Let's create a repository interface, in the Application Layer.

src/NetcoreSaas.Application/Repositories/Modules/Todo/ITaskRepository.cs

using System;
using System.Collections.Generic;
using NetcoreSaas.Domain.Models.Modules.Todo;

namespace NetcoreSaas.Application.Repositories.Modules.Todo
{
    public interface ITaskRepository: IRepository<Task>
    {
        new System.Threading.Tasks.Task<IEnumerable<Task>> GetAll();
        System.Threading.Tasks.Task<Task> Get(Guid id);
    }
}
Enter fullscreen mode Exit fullscreen mode

We're overriding the default GetAll method because we can use .Include() for future foreign key properties (e.g. as Project, CreatedByUser...).

3.2. Context DbSet

Now let's focus on our Infrastructure Layer (NetcoreSaas.Infrastructure.csproj) which is our Application implementation.

Open the BaseDbContext.cs and add a Task DbSet property.

src/NetcoreSaas.Infrastructure/Data/BaseDbContext.cs

     ...
+    public DbSet<Domain.Models.Modules.Todo.Task> Tasks { get; set; }
     public BaseDbContext(DbContextOptions options)
     {
       ...
Enter fullscreen mode Exit fullscreen mode

3.3. Repository Implementation

We can now implement our TaskRepository.cs:

src/NetcoreSaas.Infrastructure/Repositories/Modules/Todo/TaskRepository.cs

using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using NetcoreSaas.Application.Repositories.Modules.Todo;
using NetcoreSaas.Domain.Models.Modules.Todo;
using NetcoreSaas.Infrastructure.Data;
using NetcoreSaas.Infrastructure.Middleware.Tenancy;

namespace NetcoreSaas.Infrastructure.Repositories.Modules.Todo
{
    public class TaskRepository : AppRepository<Task>, ITaskRepository
    {
        public TaskRepository(BaseDbContext context, ITenantAccessService tenantAccessService = null) : base(context, tenantAccessService)
        {
        }

        public new async System.Threading.Tasks.Task<IEnumerable<Task>> GetAll()
        {
            return await Context.Tasks.ToListAsync();
        }

        public async System.Threading.Tasks.Task<Task> Get(Guid id)
        {
            return await Context.Tasks.SingleOrDefaultAsync(f=>f.Id == id);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

3.4. Unit of Work

The repositories are used by the Unit of Work instance. We need to register or ITaskRepository in our IBaseUnitOfWork interface:

src/NetcoreSaas.Application/UnitOfWork/IBaseUnitOfWork.cs

...
+ using NetcoreSaas.Application.Repositories.Modules.Todo;

namespace NetcoreSaas.Application.UnitOfWork
{
    public interface IBaseUnitOfWork
    {
         ...
+        ITaskRepository Tasks { get; }
         Task<int> CommitAsync();
    }
}
Enter fullscreen mode Exit fullscreen mode

And implement it in AppUnitOfWork.cs and MasterUnitOfWork.cs. Remember, AppUnitOfWork is for current Tenant records.

src/NetcoreSaas.Infrastructure/UnitOfWork/AppUnitOfWork.cs

...
+ using NetcoreSaas.Application.Repositories.Modules.Todo;
+ using NetcoreSaas.Infrastructure.Repositories.Modules.Todo;

namespace NetcoreSaas.Infrastructure.UnitOfWork
{
    public sealed class AppUnitOfWork : IAppUnitOfWork
    {
         ...
+        public ITaskRepository Tasks { get; }
         public AppUnitOfWork(AppDbContext context)
         {
             ...
+            Tasks = new TaskRepository(context);
         }
        ...
Enter fullscreen mode Exit fullscreen mode

MasterUnitOfWork.cs:

src/NetcoreSaas.Infrastructure/UnitOfWork/MasterUnitOfWork.cs

...
+ using NetcoreSaas.Application.Repositories.Modules.Todo;
+ using NetcoreSaas.Infrastructure.Repositories.Modules.Todo;

namespace NetcoreSaas.Infrastructure.UnitOfWork
{
    public sealed class MasterUnitOfWork : IMasterUnitOfWork
    {
         ...
+        public ITaskRepository Tasks { get; }
         public MasterUnitOfWork(MasterDbContext context)
         {
             ...
+            Tasks = new TaskRepository(context);
         }
         ...
Enter fullscreen mode Exit fullscreen mode

4. Controller

Finally, let's add the Task API methods.

4.1. URLs

Before we create the controller class, let's define our methods URLs:

src/NetcoreSaas.Domain/Helpers/ApiAppRoutes.cs

namespace NetcoreSaas.Domain.Helpers
{
    public static class ApiAppRoutes
    {
        ...
+        public static class Task
+        {
+            private const string Controller = nameof(Task);
+
+            public const string GetAll = Base + Controller + "/GetAll";
+            public const string Get = Base + Controller + "/Get/{id}";
+            public const string Create = Base + Controller + "/Create";
+            public const string Update = Base + Controller + "/Update/{id}";
+            public const string Delete = Base + Controller + "/Delete/{id}";
+        }
        ...
Enter fullscreen mode Exit fullscreen mode

4.2. The Controller

Since we want to get the current Tenant tasks we'll use the IAppUnitOfWork repositories, and use IMapper to return DTOs to the user, instead of naked entities.

src/NetcoreSaas.WebApi/Controllers/Modules/Todo/TaskController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NetcoreSaas.Application.Contracts.Modules.Todo;
using NetcoreSaas.Application.Dtos.Modules.Todo;
using NetcoreSaas.Application.UnitOfWork;
using NetcoreSaas.Domain.Helpers;
using Task = NetcoreSaas.Domain.Models.Modules.Todo.Task;

namespace NetcoreSaas.WebApi.Controllers.Modules.Todo
{
    [ApiController]
    [Authorize]
    public class TaskController : ControllerBase
    {
        private readonly IMapper _mapper;
        private readonly IAppUnitOfWork _appUnitOfWork;

        public TaskController(IMapper mapper, IAppUnitOfWork appUnitOfWork)
        {
            _mapper = mapper;
            _appUnitOfWork = appUnitOfWork;
        }

        [HttpGet(ApiAppRoutes.Task.GetAll)]
        public async Task<IActionResult> GetAll()
        {
            var records = await _appUnitOfWork.Tasks.GetAll();
            if (!records.Any())
                return NoContent();
            return Ok(_mapper.Map<IEnumerable<TaskDto>>(records));
        }

        [HttpGet(ApiAppRoutes.Task.Get)]
        public async Task<IActionResult> Get(Guid id)
        {
            var record = await _appUnitOfWork.Tasks.Get(id);
            if (record == null)
                return NotFound();
            return Ok(_mapper.Map<TaskDto>(record));
        }

        [HttpPost(ApiAppRoutes.Task.Create)]
        public async Task<IActionResult> Create([FromBody] CreateTaskRequest request)
        {
            var task = new Task()
            {
                Name = request.Name,
                Priority = request.Priority,
            };
            _appUnitOfWork.Tasks.Add(task);
            if (await _appUnitOfWork.CommitAsync() == 0)
                return BadRequest();

            task = await _appUnitOfWork.Tasks.Get(task.Id);
            return Ok(_mapper.Map<TaskDto>(task));
        }

        [HttpPut(ApiAppRoutes.Task.Update)]
        public async Task<IActionResult> Update(Guid id, [FromBody] UpdateTaskRequest request)
        {
            var existing = _appUnitOfWork.Tasks.GetById(id);
            if (existing == null)
                return NotFound();

            existing.Name = request.Name;
            existing.Priority = request.Priority;

            if (await _appUnitOfWork.CommitAsync() == 0)
                return BadRequest("api.errors.noChanges");

            existing = await _appUnitOfWork.Tasks.Get(id);
            return Ok(_mapper.Map<TaskDto>(existing));
        }

        [HttpDelete(ApiAppRoutes.Task.Delete)]
        public async Task<IActionResult> Delete(Guid id)
        {
            var record = _appUnitOfWork.Tasks.GetById(id);
            if (record == null)
                return NotFound();

            _appUnitOfWork.Tasks.Remove(record);
            if (await _appUnitOfWork.CommitAsync() == 0)
                return BadRequest();

            return NoContent();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

5. Migrations

Open terminal and generate the initial migration in the MasterDbContext.

If you already have an Initial migration, delete the WebApi.Migrations folder or change the Initial migration name.

cd src/NetcoreSaas.WebApi
dotnet ef migrations add Initial --context MasterDbContext
Enter fullscreen mode Exit fullscreen mode

Before you apply the migration, make sure to have the following environment variables set, in appSettings.Development.json:

  • ProjectConfiguration.MultiTenancySingleDatabase
  • ProjectConfiguration.MasterDatabasetodo-db-dev
  • ConnectionStrings.DbContext_PostgreSQL → Your postgres server, e.g. Server=localhost;Port=5432;Uid=testing;Pwd=testing;Database=[DATABASE];
  • DefaultUsers.Email.DEVELOPMENT_ADMIN_EMAILadmin@admin.com
  • DefaultUsers.Password.DEVELOPMENT_ADMIN_PASSWORDadmin123

Update the database:

dotnet ef database update --context MasterDbContext
Enter fullscreen mode Exit fullscreen mode

Ignore the warning: 42P07: relation "AuditLogs" already exists.

Verify that your server has the todo-db-dev database with the Tasks table:

Database Todo Database

6. Run the application

Let's test our To-Do application.

6.1. Debug the API

Start the API with the NetcoreSaas.WebApi: NetcoreSaas configuration.

6.2. Run the ClientApp

Open the ClientApp folder in VSCode.

Update the VITE_VUE_APP_SERVICE environment variable to api:

ClientApp/.env.development

- VITE_VUE_APP_SERVICE=sandbox
+ VITE_VUE_APP_SERVICE=api
Enter fullscreen mode Exit fullscreen mode

Open the terminal and run:

yarn serve
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:3000 and log in with the following credentials:

Login

6.3. Add, Edit and Delete Tasks

Click on Switch to app sidebar item and then Tasks.

You will now be able to perform Tasks CRUD operations:

Tasks


If you have the SaasFrontends Vue3 essential edition, you can ask for the code.

In Part 3 - Database per Tenant we'll change to the Database per Tenant strategy.

Top comments (0)