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
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
}
}
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; }
}
}
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.
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; }
}
}
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>();
}
...
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; }
}
}
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; }
}
}
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);
}
}
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)
{
...
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);
}
}
}
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();
}
}
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);
}
...
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);
}
...
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}";
+ }
...
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();
}
}
}
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 →
SingleDatabase
-
ProjectConfiguration.MasterDatabase →
todo-db-dev
-
ConnectionStrings.DbContext_PostgreSQL → Your postgres server, e.g.
Server=localhost;Port=5432;Uid=testing;Pwd=testing;Database=[DATABASE];
-
DefaultUsers.Email.DEVELOPMENT_ADMIN_EMAIL →
admin@admin.com
-
DefaultUsers.Password.DEVELOPMENT_ADMIN_PASSWORD →
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:
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
Open the terminal and run:
yarn serve
Open http://localhost:3000 and log in with the following credentials:
- Email: admin@admin.com
- Password: admin123
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:
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)