Build a To-Do app - Part 2 - .NET API
Tags: vue3, dotnet, saas, tutorial
Published: 2022-03-05 21:05:40

---
title: Build a To-Do app - Part 2 - .NET API
published: true
description: In this tutorial, we'll use the SaasFrontends Vue3 codebase to build a basic To-Do app with Tasks, Routing, Model, and CRUD Components.
tags: vue3,dotnet,saas,tutorial
cover_image: https://yahooder.sirv.com/saasfrontends/tutorials/build-a-to-do-app/todo-2-net-api.png
---

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

- [Part 1 - Frontend](https://dev.to/alexandromtzg/build-a-to-do-app-part-1-vue3-app-51m0)
- [Part 2 - Backend](https://dev.to/alexandromtzg/build-a-to-do-app-part-2-net-api-3j50) (Current)
- [Part 3 - Database per Tenant](https://dev.to/alexandromtzg/build-a-to-do-app-part-3-database-per-tenant-4l9p)

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

## Requirements

- Any SaasFrontends [Essential](https://alexandromg.gumroad.com) or [Premium](https://alexandromg.gumroad.com) edition.

---

## Steps

1. <a href="#1-domain-model" className='font-normal border-dashed'>Domain Model</a>
2. <a href="#2-application-dt-os" className='font-normal border-dashed'>Application DTOs</a>
3. <a href="#3-task-repository" className='font-normal border-dashed'>Task Repository</a>
4. <a href="#4-controller" className='font-normal border-dashed'>Controller</a>
5. <a href="#5-migrations" className='font-normal border-dashed'>Migrations</a>
6. <a href="#6-run-the-application" className='font-normal border-dashed'>Run the Application</a>


## 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
```js
namespace NetcoreSaas.Domain.Enums.Modules.Todo
{
    public enum TaskPriority
    {
        Low,
        Medium,
        High
    }
}
```

### 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
```js
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; }
    }
}
```

## 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](https://stackoverflow.com/a/21569720/5697060)

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
```js
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; }
    }
}
```

### 2.2. Mapper Configuration

src/NetcoreSaas.Application/Mapper/MappingProfile.cs
```diff
...
+ 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>();
        }
        ...
```

### 2.3. CreateTaskRequest.cs contract

src/NetcoreSaas.Application/Contracts/Modules/Todo/CreateTaskRequest.cs
```js
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; }
    }
}
```

### 2.4. UpdateTaskRequest.cs contract

src/NetcoreSaas.Application/Contracts/Modules/Todo/UpdateTaskRequest.cs
```js
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; }
    }
}
```

## 3. Task Repository

If you're just getting started on the Repository Pattern, I recommend you to read [Mosh Hamedani content](https://www.youtube.com/watch?v=rtXpYpZdOzM).

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
```js
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);
    }
}
```

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
```diff
     ...
+    public DbSet<Domain.Models.Modules.Todo.Task> Tasks { get; set; }
     public BaseDbContext(DbContextOptions options)
     {
       ...
```

### 3.3. Repository Implementation

We can now implement our ***TaskRepository.cs***:

src/NetcoreSaas.Infrastructure/Repositories/Modules/Todo/TaskRepository.cs
```js
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);
        }
    }
}
```

### 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
```diff
...
+ using NetcoreSaas.Application.Repositories.Modules.Todo;

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

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

src/NetcoreSaas.Infrastructure/UnitOfWork/AppUnitOfWork.cs
```diff
...
+ 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);
         }
        ...
```

***MasterUnitOfWork.cs***:

src/NetcoreSaas.Infrastructure/UnitOfWork/MasterUnitOfWork.cs
```diff
...
+ 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);
         }
         ...
```

## 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
```diff
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}";
+        }
        ...
```

### 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
```js
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();
        }
    }
}
```

## 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
```

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

- **ProjectConfiguration.MultiTenancy** &rarr; `SingleDatabase`
- **ProjectConfiguration.MasterDatabase** &rarr; `todo-db-dev`
- **ConnectionStrings.DbContext_PostgreSQL** &rarr; Your postgres server, e.g. `Server=localhost;Port=5432;Uid=testing;Pwd=testing;Database=[DATABASE];`
- **DefaultUsers.Email.DEVELOPMENT_ADMIN_EMAIL** &rarr; `admin@admin.com`
- **DefaultUsers.Password.DEVELOPMENT_ADMIN_PASSWORD** &rarr; `admin123`

Update the database:

```
dotnet ef database update --context MasterDbContext
```

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](https://yahooder.sirv.com/saasfrontends/tutorials/build-a-to-do-app/todo-app-part-2/database-todo-db.png)

## 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
```diff
- VITE_VUE_APP_SERVICE=sandbox
+ VITE_VUE_APP_SERVICE=api
```

Open the terminal and run:

```
yarn serve
```

Open [http://localhost:3000](http://localhost:3000) and log in with the following credentials:

- Email: admin@admin.com
- Password: admin123

![Login](https://yahooder.sirv.com/saasfrontends/tutorials/build-a-to-do-app/todo-app-part-2/login.png)

### 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](https://yahooder.sirv.com/saasfrontends/tutorials/build-a-to-do-app/todo-app-part-2/tasks.png)

---

If you have the **SaasFrontends Vue3 essential** edition, [you can ask for the code](https://saasfrontends.com/docs/support).

[In Part 3 - Database per Tenant](https://dev.to/alexandromtzg/build-a-to-do-app-part-3-database-per-tenant-4l9p) we'll change to the ***Database per Tenant*** strategy.