TODO Application Tutorial with Layered Solution
This is a single-part quick-start tutorial to build a simple todo application with the ABP. Here's a screenshot from the final application:
You can find the source code of the completed application here.
This documentation has a video tutorial on YouTube!! You can watch it here:
Pre-Requirements
An IDE (e.g. Visual Studio) that supports .NET 9.0+ development.
Install ABP CLI Tool
We will use the ABP CLI to create new ABP solutions. You can run the following command on a terminal window to install this dotnet tool:
dotnet tool install -g Volo.Abp.Studio.Cli
Create Your ABP Solution
Create an empty folder, open a command-line terminal and execute the following command in the terminal:
abp new TodoApp -d mongodb
This will create a new solution, named TodoApp. Once the solution is ready, open it in your favorite IDE.
Create the Database
If you are using Visual Studio, right click on the TodoApp.DbMigrator
project, select Set as StartUp Project, then hit Ctrl+F5 to run it without debugging. It will create the initial database and seed the initial data.
Before Running the Application
Installing the Client-Side Packages
ABP CLI runs the abp install-libs
command behind the scenes to install the required NPM packages for your solution while creating the application.
However, sometimes this command might need to be manually run. For example, you need to run this command, if you have cloned the application, or the resources from node_modules folder didn't copy to wwwroot/libs folder, or if you have added a new client-side package dependency to your solution.
For such cases, run the abp install-libs
command on the root directory of your solution to install all required NPM packages:
abp install-libs
We suggest you install Yarn v1.22+ (not v2) to prevent possible package inconsistencies, if you haven't installed it yet.
Run the Application
It is good to run the application before starting the development. Ensure the TodoApp.Web
project is the startup project, then run the application (Ctrl+F5 in Visual Studio) to see the initial UI:
You can click on the Login button, use admin
as the username and 1q2w3E*
as the password to login to the application.
All ready. We can start coding!
Domain Layer
This application has a single entity and we'll start by creating it. Create a new TodoItem
class inside the TodoApp.Domain project:
using System;
using Volo.Abp.Domain.Entities;
namespace TodoApp
{
public class TodoItem : BasicAggregateRoot<Guid>
{
public string Text { get; set; } = string.Empty;
}
}
BasicAggregateRoot
is the simplest base class to create root entities, and Guid
is the primary key (Id
) of the entity here.
Database Integration
Next step is to setup the MongoDB configuration. Open the TodoAppMongoDbContext
class in the MongoDb
folder of the TodoApp.MongoDB project and make the following changes:
- Add a new property to the class:
public IMongoCollection<TodoItem> TodoItems => Collection<TodoItem>();
- Add the following code inside the
CreateModel
method:
modelBuilder.Entity<TodoItem>(b =>
{
b.CollectionName = "TodoItems";
});
Now, we can use the ABP repositories to save and retrieve the todo items, as we'll do in the next section.
Application Layer
An Application Service is used to perform the use cases of the application. We need to perform the following use cases:
- Get the list of the todo items
- Create a new todo item
- Delete an existing todo item
Application Service Interface
We can start by defining an interface for the application service. Create a new ITodoAppService
interface in the TodoApp.Application.Contracts project, as shown below:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace TodoApp
{
public interface ITodoAppService : IApplicationService
{
Task<List<TodoItemDto>> GetListAsync();
Task<TodoItemDto> CreateAsync(string text);
Task DeleteAsync(Guid id);
}
}
Data Transfer Object
GetListAsync
and CreateAsync
methods return TodoItemDto
. ApplicationService
typically gets and returns DTOs (Data Transfer Objects) instead of entities. So, we should define the DTO class here. Create a new TodoItemDto
class inside the TodoApp.Application.Contracts project:
using System;
namespace TodoApp
{
public class TodoItemDto
{
public Guid Id { get; set; }
public string Text { get; set; } = string.Empty;
}
}
This is a very simple DTO class that matches our TodoItem
entity. We are ready to implement the ITodoAppService
.
Application Service Implementation
Create a TodoAppService
class inside the TodoApp.Application project, as shown below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace TodoApp
{
public class TodoAppService : ApplicationService, ITodoAppService
{
private readonly IRepository<TodoItem, Guid> _todoItemRepository;
public TodoAppService(IRepository<TodoItem, Guid> todoItemRepository)
{
_todoItemRepository = todoItemRepository;
}
// TODO: Implement the methods here...
}
}
This class inherits from the ApplicationService
class of the ABP and implements the ITodoAppService
that was defined before. ABP provides default generic repositories for the entities. We can use them to perform the fundamental database operations. This class injects IRepository<TodoItem, Guid>
, which is the default repository for the TodoItem
entity. We will use it to implement the use cases described before.
Getting Todo Items
Let's start by implementing the GetListAsync
method:
public async Task<List<TodoItemDto>> GetListAsync()
{
var items = await _todoItemRepository.GetListAsync();
return items
.Select(item => new TodoItemDto
{
Id = item.Id,
Text = item.Text
}).ToList();
}
We are simply getting the complete TodoItem
list from the database, mapping them to TodoItemDto
objects and returning as the result.
Creating a New Todo Item
Next method is CreateAsync
and we can implement it as shown below:
public async Task<TodoItemDto> CreateAsync(string text)
{
var todoItem = await _todoItemRepository.InsertAsync(
new TodoItem {Text = text}
);
return new TodoItemDto
{
Id = todoItem.Id,
Text = todoItem.Text
};
}
The repository's InsertAsync
method inserts the given TodoItem
to the database and returns the same TodoItem
object. It also sets the Id
, so we can use it on the returning object. We are simply returning a TodoItemDto
by creating from the new TodoItem
entity.
Deleting a Todo Item
Finally, we can implement the DeleteAsync
as the following code block:
public async Task DeleteAsync(Guid id)
{
await _todoItemRepository.DeleteAsync(id);
}
The application service is ready to be used from the UI layer.
User Interface Layer
It is time to show the todo items on the UI! Before starting to write the code, it would be good to remember what we are trying to build. Here's a sample screenshot from the final UI:
We will keep the UI side minimal for this tutorial to make the tutorial simple and focused. See the web application development tutorial to build real-life pages with all aspects.
Index.cshtml.cs
Open the Index.cshtml.cs
file in the Pages
folder of the TodoApp.Web project and replace the content with the following code block:
using System.Collections.Generic;
using System.Threading.Tasks;
namespace TodoApp.Web.Pages
{
public class IndexModel : TodoAppPageModel
{
public List<TodoItemDto> TodoItems { get; set; }
private readonly ITodoAppService _todoAppService;
public IndexModel(ITodoAppService todoAppService)
{
_todoAppService = todoAppService;
}
public async Task OnGetAsync()
{
TodoItems = await _todoAppService.GetListAsync();
}
}
}
This class uses the ITodoAppService
to get the list of todo items and assign the TodoItems
property. We will use it to render the todo items on the razor page.
Index.cshtml
Open the Index.cshtml
file in the Pages
folder of the TodoApp.Web project and replace it with the following content:
@page
@model TodoApp.Web.Pages.IndexModel
@section styles {
<abp-style src="/Pages/Index.css" />
}
@section scripts {
<abp-script src="/Pages/Index.js" />
}
<div class="container">
<abp-card>
<abp-card-header>
<abp-card-title>
TODO LIST
</abp-card-title>
</abp-card-header>
<abp-card-body>
<!-- FORM FOR NEW TODO ITEMS -->
<form id="NewItemForm" class="row row-cols-lg-auto g-3 align-items-center">
<div class="col-12">
<div class="input-group">
<input id="NewItemText" type="text" class="form-control" placeholder="enter text...">
</div>
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</form>
<!-- TODO ITEMS LIST -->
<ul id="TodoList">
@foreach (var todoItem in Model.TodoItems)
{
<li data-id="@todoItem.Id">
<i class="fa fa-trash-o"></i> @todoItem.Text
</li>
}
</ul>
</abp-card-body>
</abp-card>
</div>
We are using ABP's card tag helper to create a simple card view. You could directly use the standard bootstrap HTML structure, however the ABP tag helpers make it much easier and type safe.
This page imports a CSS and a JavaScript file, so we should also create them.
Index.js
Create a file named Index.js
in the Pages
folder of the TodoApp.Web project and replace it with the following content:
$(function () {
// DELETING ITEMS /////////////////////////////////////////
$('#TodoList').on('click', 'li i', function(){
var $li = $(this).parent();
var id = $li.attr('data-id');
todoApp.todo.delete(id).then(function(){
$li.remove();
abp.notify.info('Deleted the todo item.');
});
});
// CREATING NEW ITEMS /////////////////////////////////////
$('#NewItemForm').submit(function(e){
e.preventDefault();
var todoText = $('#NewItemText').val();
todoApp.todo.create(todoText).then(function(result){
$('<li data-id="' + result.id + '">')
.html('<i class="fa fa-trash-o"></i> ' + result.text)
.appendTo($('#TodoList'));
$('#NewItemText').val('');
});
});
});
In the first part, we are subscribing to the click events of the trash icons near the todo items, deleting the related item on the server and showing a notification on the UI. Also, we are removing the deleted item from the DOM, so we don't need to refresh the page.
In the second part, we are creating a new todo item on the server. If it succeeds, we are then manipulating the DOM to insert a new <li>
element to the todo list. This way we don't need to refresh the whole page after creating a new todo item.
The interesting part here is how we communicate with the server. See the Dynamic JavaScript Proxies & Auto API Controllers section to understand how it works. But now, let's continue and complete the application.
Index.css
As the final touch, Create a file named Index.css
in the Pages
folder of the TodoApp.Web project and replace it with the following content:
#TodoList{
list-style: none;
margin: 0;
padding: 0;
}
#TodoList li {
padding: 5px;
margin: 5px 0px;
}
#TodoList li i
{
opacity: 0.5;
}
#TodoList li i:hover
{
opacity: 1;
color: #ff0000;
cursor: pointer;
}
This is a simple styling for the todo page. We believe that you can do much better :)
Now, you can run the application again and see the result.
Dynamic JavaScript Proxies & Auto API Controllers
In the Index.js
file, we've used the todoApp.todo.delete(...)
and todoApp.todo.create(...)
functions to communicate with the server. These functions are dynamically created by the ABP, thanks to the Dynamic JavaScript Client Proxy system. They perform HTTP API calls to the server and return a promise, so you can register a callback to the then
function as we've done above.
However, you may notice that we haven't created any API Controllers, so how does the server handle these requests? This question brings us to the Auto API Controller feature of the ABP. It automatically converts the application services to API Controllers by convention.
If you open the Swagger UI by entering the /swagger
URL in your application, you can see the Todo API:
Conclusion
In this tutorial, we've built a very simple application to warm up for the ABP. If you are looking to build a serious application, please check the web application development tutorial which covers all the aspects of real-life web application development.
Source Code
You can find source code of the completed application here.