ASP.NET Zero Step by Step Development Guideline

How to create a new feature on ASP.NET Zero (Multi-tenant)

Quang Trong VU
Old Dev
20 min readMar 25, 2020

--

This guide includes steps as below

1- Setup Development Environment

2- Step by step development guideline

1- Setup Environment

Please refer to the original links:

2- Step by step development guideline

*We use the default login info as Default tenancy name, username admin, password 123qwe

Multi-tenant Login Page

(1) This guide starts with preconfigured multi-tenant support (MultiTenancyEnabled = true) in file ./Core.Shared.AbpZeroTemplateConsts

public class AbpZeroTemplateConsts
{
public const string LocalizationSourceName = "AbpZeroTemplate";
public const string ConnectionStringName = "Default";
public const bool MultiTenancyEnabled = true;
public const bool AllowTenantsToChangeEmailSettings = false;
public const string Currency = "USD";
public const string CurrencySign = "$";
public const string AbpApiClientUserAgent = "AbpApiClient";
}

(2) Defining a Menu Item (on Angular project)

(2.1) Open src/app/shared/layout/nav/app-navigation.service.ts in the client-side which defines menu items in the application. Create a new menu item as shown below

new AppMenuItem("Learning", null, "flaticon-book", "/app/main/learning")
app-navigation

Learning is the menu name (will localize below), null is for permission name (will set later), /app/main/learning is the Angular route.

(2.2) Localize Menu Item Display Name

Go to .Core project

Add for default localization file .Core/Localization/AbpZeroTemplate/AbpZeroTemplate.xml

<text name="Learning">Learning</text>
Localize Menu Item

Add for Vietnamese localization file AbpZeroTemplate-vi.xml

Localize Menu Item — Vietnamese

Note: Any change in server-side (including change localization texts) requires to recycle of the server application. You SHOULD use Ctrl + F5 if you don’t need to debug for a faster startup.

(2.3) Angular Route

open src/app/main/main-routing.module.ts in the client-side and add a new route

{ path: 'learning', component: LearningComponent }
Add Route
{ path: ‘**’, redirectTo: ‘dashboard’} always come last

Note: You must place your route above the “others” route. (path: ‘**’)

(3) Create LearningComponent (on Angular project)

Create a learning folder inside src/app/main folder and add a new typescript file (learning.component.ts) in the student folder as shown below:

import { Component, Injector } from '@angular/core';
import { AppComponentBase } from '@shared/common/app-component-base';
import { appModuleAnimation } from '@shared/animations/routerTransition';
@Component({
templateUrl: './learning.component.html',
animations: [appModuleAnimation()]
})
export class LearningComponent extends AppComponentBase {
constructor(
injector: Injector
) {
super(injector);
}
}
Create student component

We inherited from AppComponentBase which provides some common functions and fields (like localization and access control). It’s not required, but it makes our job easier. Now we will import new component to main-routing.module.ts

import { LearningComponent } from './learning/learning.component';
Import to Main Routing Module

Create a template for student component learning.component.html

<div [@routerTransition]>
<div class="kt-content kt-grid__item kt-grid__item--fluid kt-grid kt-grid--hor">
<div class="kt-subheader kt-grid__item">
<div class="kt-container ">
<div class="kt-subheader__main">
<h3 class="kt-subheader__title">
<span>{{"Learning" | localize}}</span>
</h3>
</div>
</div>
</div>
<div class="kt-container kt-grid__item kt-grid__item--fluid">
<div class="kt-portlet kt-portlet--mobile">
<div class="kt-portlet__body kt-portlet__body--fit">
<p>STUDENT CONTENT COMES HERE!</p>
</div>
</div>
</div>
</div>
</div>

We use pipe to perform localize. {{“Learning” | localize}}

[@routeTransition] attribute is required for page transition animation

And finally, we add this component to main.module.ts.

...
import { LearningComponent } from './learning/learning.component';
...
declarations: [
...
LearningComponent
]
Add Component to Module
Learning View

(4) Create Student Entity

We must define entities in .Core project (server-side). This project defines Student entity (mapped to PbStudents table in database). We create a new folder Learning.

Student Entity

You should define constants as below

public class PersonConsts
{
public const int MaxNameLength = 32;
public const int MaxSurnameLength = 32;
public const int MaxEmailAddressLength = 255;
}
public class PhoneConsts
{
public const int MaxNumberLength = 16;
}
public class StudentConsts
{
public const int MaxNameLength = 32;
public const int MaxSurnameLength = 32;
public const int MaxEmailAddressLength = 255;
}
Define Entity Constants

Add a DbSet property for Student entity to DbContext in project .EntityFrameworkCore

public virtual DbSet<Student> Students { get; set; }
Add DbSet

(5) Database Migrations for Student

We use EntityFramework Core Code-First migration to migrate database schema. Since we added Student entity, our DbContext model is changed. So, we have to create a new migration to create the new table in database.

Go to .EntityFrameworkCore project in Terminal.

Run

dotnet ef migrations add "Added_Student_Table"

Check the generated file in .EntityFrameworkCore/Migrations

Data migration generated files

Run command to apply the new migration to Create new DbStudents table in Database

dotnet ef database update
ef update database
Generated database table

Now we will seed initial data for this table

Add InitialStudentCreator.cs to ./EntityFrameworkCore/Migrations/Seed/Host

*Note: we set TenantId = 1, for default admin

public class InitialStudentCreator
{
private readonly AbpZeroTemplateDbContext _context;
public InitialStudentCreator(AbpZeroTemplateDbContext context)
{
_context = context;
}
public void Create()
{
var douglas = _context.Students.FirstOrDefault(p => p.EmailAddress == "douglas.adams@fortytwo.com");
if (douglas == null)
{
_context.Students.Add(
new Student
{
Name = "Douglas",
Surname = "Adams",
EmailAddress = "douglas.adams@fortytwo.com",
TenantId = 1
});
}
var asimov = _context.Students.FirstOrDefault(p => p.EmailAddress == "isaac.asimov@foundation.org");
if (asimov == null)
{
_context.Students.Add(
new Student
{
Name = "Isaac",
Surname = "Asimov",
EmailAddress = "isaac.asimov@foundation.org",
TenantId = 1
});
}
}
}
Seed data for Student table

This default data is good because we can also use these data in Unit Test. We should be careful about seed data since this code will always be executed in each PostInitilize

Add to HostDbBuilder

(6) Create Unit Tests for Student Application Service

Create Student test case as follow

*We will create Unit Test before creating Services & Dtos

public class StudentAppService_Tests : AppTestBase
{
private readonly IStudentAppService _studentAppService;
public StudentAppService_Tests()
{
_studentAppService = Resolve<IStudentAppService>();
}
[Fact]
public void Should_Get_All_People_Without_Any_Filter()
{
//Act
var students = _studentAppService.GetPeople(new GetStudentInput());
//Assert
students.Items.Count.ShouldBe(2);
}
[Fact]
public void Should_Get_Student_With_Filter()
{
//Act
var persons = _studentAppService.GetPeople(
new GetStudentInput
{
Filter = "Douglas"
});
//Assert
persons.Items.Count.ShouldBe(1);
persons.Items[0].Name.ShouldBe("Douglas");
persons.Items[0].Surname.ShouldBe("Adams");
}
[Fact]
public async Task Should_Create_Student_With_Valid_Arguments()
{
//Act
await _studentAppService.CreatePerson(
new CreateStudentInput
{
Name = "John",
Surname = "Nash",
EmailAddress = "john.nash@abeautifulmind.com"
});
//Assert
UsingDbContext(
context =>
{
var john = context.Students.FirstOrDefault(p => p.EmailAddress == "john.nash@abeautifulmind.com");
john.ShouldNotBe(null);
john.Name.ShouldBe("John");
});
}
[Fact]
public async Task Should_Not_Create_Student_With_Invalid_Arguments()
{
//Act and Assert
await Assert.ThrowsAsync<AbpValidationException>(
async () =>
{
await _studentAppService.CreatePerson(
new CreatePersonInput
{
Name = "John"
});
});
}
}
Student Test Case
Student Test Case

We derived test class from AppTestBase. AppTestBase class initializes all system, creates an in-memory fake database, seeds initial data (that we created before) to database and logins to application as admin. So, this is actually an integration test since it tests all server-side code from entity framework mapping to application services, validation and authorization.

(6.1) Create Service

An Application Service is used from client (Presentation Layer, Angular in this case) to perform operations (use cases) of the application.

Application Service interface and DTOs are located in .Application.Shared project.

  • First, we create IStudentAppService
public interface IStudentAppService : IApplicationService
{
ListResultDto<StudentListDto> GetStudent(GetStudentInput input);
Task CreateStudent(CreateStudentInput input);
}
  • Next, we create the required Dtos: StudentListDto.cs
public class StudentListDto : FullAuditedEntityDto
{
public string Name { get; set; }
public string Surname { get; set; } public string EmailAddress { get; set; }
}
  • GetStudentInput.cs
public class GetStudentInput
{
public string Filter { get; set; }
}
  • GetStudentForEditInput.cs
public class GetStudentForEditInput
{
public int Id { get; set; }
}
  • GetStudentForEditOutput.cs
public class GetStudentForEditOutput
{
public int Id { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
public string EmailAddress { get; set; }
}
  • CreateStudentInput.cs
public class CreateStudentInput
{
[Required]
[MaxLength(StudentConsts.MaxNameLength)]
public string Name { get; set; }
[Required]
[MaxLength(StudentConsts.MaxSurnameLength)]
public string Surname { get; set; }
[EmailAddress]
[MaxLength(StudentConsts.MaxEmailAddressLength)]
public string EmailAddress { get; set; }
}
IStudentAppService and Dtos

Map Entity and Dto with CustomDtoMapper.cs in project .Application

configuration.CreateMap<Student, StudentListDto>();
configuration.CreateMap<CreateStudentInput, Student>();
configuration.CreateMap<Student, GetStudentForEditOutput>();
Update CustomDtoMapper.cs

Now we will add the implementation of IStudentAppService to project .Application

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Abp.Collections.Extensions;
using Abp.Domain.Repositories;
using Abp.Extensions;
using Abp.Application.Services.Dto;
using MyCompanyName.AbpZeroTemplate.Learning.Dto;
namespace MyCompanyName.AbpZeroTemplate.Learning
{
public class StudentAppService : AbpZeroTemplateAppServiceBase,
IStudentAppService
{
private readonly IRepository<Student> _studentRepository;
public StudentAppService(IRepository<Student> studentRepository)
{
_studentRepository = studentRepository;
}
public ListResultDto<StudentListDto> GetStudent(GetStudentInput input)
{
var students = _studentRepository
.GetAll()
.WhereIf(
!input.Filter.IsNullOrEmpty(),
p => p.Name.Contains(input.Filter) ||
p.Surname.Contains(input.Filter) ||
p.EmailAddress.Contains(input.Filter)
)
.OrderBy(p => p.Name)
.ThenBy(p => p.Surname)
.ToList();
return new ListResultDto<StudentListDto>(ObjectMapper.
Map<List<StudentListDto>>(students));
}
public async Task CreateStudent(CreateStudentInput input)
{
var student = ObjectMapper.Map<Student>(input);
await _studentRepository.InsertAsync(student);
}
}
}

Here, we’re injecting student repository (it’s automatically created by ABP) and using it to filter and get student from database

WhereIf is an extension method here (defined in Abp.Linq.Extensions namespace). It performs Where condition, only if filter is not null or empty. IsNullOrEmpty is also an extension method (defined in Abp.Extensions namespace). ABP has many similar shortcut extension methods. ObjectMapper.Map method automatically converts list of Student entities to list of StudentListDto objects with using configurations in CustomDtoMapper.cs in .Application project.

Connection & Transaction Management

We don’t manually open database connection or start/commit transactions manually. It’s automatically done with ABP framework’s Unit Of Work system.

Exception Handling

We don’t handle exceptions manually (using a try-catch block). Because ABP framework automatically handles all exceptions on the web layer and returns appropriate error messages to the client. It then handles errors on the client and shows needed error information to the user.

(6.2) Ok. Now we get back to run Unit Test

Run Unit Test

(7) Using GetPeople Method From Angular Component

Now we will use the GetPeople method on the client site to display Student list.

Service Proxy Generation

First, run (prefer Ctrl+F5 to be faster) the server side application (.Web.Host project). Then run nswag/refresh.bat file on the client side to re-generate service proxy classes (they are used to call server side service methods).
*Should update config file to the port of back end.

Update backend port before run nswag

Now run refresh command

cd nswag/
../node_modules/.bin/nswag run
re-generate mapping

Since we added a new service, we should add it to src/shared/service-proxies/service-proxy.module.ts. Just open it and add ApiServiceProxies.PersonServiceProxy to the providers array. This step is only required when we add a new service. If we change an existing service, it’s not needed.

Add to service-proxy.module.ts

Angular-Cli Watcher

Sometimes angular-cli can not understand the file changes. In that case, stop it and re-run npm start command.

LearningComponent Typescript Class

Update learning.component.ts as below

import { Component, Injector, OnInit } from '@angular/core';
import { AppComponentBase } from '@shared/common/app-component-base';
import { appModuleAnimation } from '@shared/animations/routerTransition';
import { StudentServiceProxy, StudentListDto, ListResultDtoOfStudentListDto } from '@shared/service-proxies/service-proxies';
@Component({
templateUrl: './learning.component.html',
animations: [appModuleAnimation()]
})
export class LearningComponent extends AppComponentBase implements OnInit {
student: StudentListDto[] = [];
filter: string = '';
constructor(
injector: Injector,
private _studentService: StudentServiceProxy
) {
super(injector);
}
ngOnInit(): void {
this.getPeople();
}
getPeople(): void {
this._studentService.getStudent(this.filter).subscribe((result) => {
this.student = result.items;
});
}
}

We inject StudentServiceProxy, call its getStudent method and subscribe to get the result. We do this in ngOnInit function (defined in Angular’s OnInit interface). Assigned returned items to the student class member.

Rendering People In Angular View

Now, we can use this people member from the view, learning.component.html

<div [@routerTransition]>
<div class="kt-content kt-grid__item kt-grid__item--fluid kt-grid kt-grid--hor">
<div class="kt-subheader kt-grid__item">
<div class="kt-container ">
<div class="kt-subheader__main">
<h3 class="kt-subheader__title">
<span>{{"Learning" | localize}}</span>
</h3>
</div>
</div>
</div>
<div class="kt-container kt-grid__item kt-grid__item--fluid">
<div class="kt-portlet kt-portlet--mobile">
<div class="kt-portlet__body kt-portlet__body--fit">
<h3>{{"AllStudent" | localize}}</h3>
<div *ngFor="let person of student">
<div class="row kt-row--no-padding align-items-center">
<div class="col">
<h4>{{person.name + ' ' + person.surname}}</h4>
<span>{{person.emailAddress}}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

You can see the output as below

Student List

(8) Creating a New Person

create create-student-modal.component.ts file in ./angular/src/app/main/learning/ folder.

import { Component, ViewChild, Injector, ElementRef, Output, EventEmitter } from '@angular/core';
import { ModalDirective } from 'ngx-bootstrap';
import { StudentServiceProxy, CreateStudentInput } from '@shared/service-proxies/service-proxies';
import { AppComponentBase } from '@shared/common/app-component-base';
import { finalize } from 'rxjs/operators';
@Component({
selector: 'createStudentModal',
templateUrl: './create-student-modal.component.html'
})
export class CreateStudentModalComponent extends AppComponentBase {
@Output() modalSave: EventEmitter<any> = new EventEmitter<any>(); @ViewChild('modal' , { static: false }) modal: ModalDirective;
@ViewChild('nameInput' , { static: false }) nameInput: ElementRef;
student: CreateStudentInput = new CreateStudentInput(); active: boolean = false;
saving: boolean = false;
constructor(
injector: Injector,
private _studentService: StudentServiceProxy
) {
super(injector);
}
show(): void {
this.active = true;
this.student = new CreateStudentInput();
this.modal.show();
}
onShown(): void {
this.nameInput.nativeElement.focus();
}
save(): void {
this.saving = true;
this._studentService.createStudent(this.student)
.pipe(finalize(() => this.saving = false))
.subscribe(() => {
this.notify.info(this.l('SavedSuccessfully'));
this.close();
this.modalSave.emit(this.student);
});
}
close(): void {
this.modal.hide();
this.active = false;
}
}

Let me explain some parts of this class:

  • It has a selector, createStudentModal, which will be used as like an HTML element in the student list page.
  • It extends AppComponentBase to take advantage of it (localize and access control).
  • Defines an event, modalSave, which is triggered when we successfully save the modal. Thus, the main page will be informed and it can reload the student list.
  • Declares two ViewChild members (modal and nameInput) to access some elements in the view.
  • Injects StudentServiceProxy to call server side method while creating the student.
  • It focuses to name input when modal is shown.

The code is simple and easy to understand except a small hack: an active flag is used to reset validation for Angular view (explained in angular’s documentation).

As declared in the component, we are creating the create-student-modal.component.html file in the same folder as shown below:

<div bsModal #modal="bs-modal" (onShown)="onShown()" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="modal" aria-hidden="true" [config]="{backdrop: 'static'}">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<form *ngIf="active" #studentForm="ngForm" novalidate (ngSubmit)="save()">
<div class="modal-header">
<h4 class="modal-title">
<span>{{"CreateNewStudent" | localize}}</span>
</h4>
<button type="button" class="close" (click)="close()" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>{{"Name" | localize}}</label>
<input #nameInput class="form-control" type="text" name="name" [(ngModel)]="student.name" required maxlength="32">
</div>
<div class="form-group">
<label>{{"Surname" | localize}}</label>
<input class="form-control" type="text" name="surname" [(ngModel)]="student.surname" required maxlength="32">
</div>
<div class="form-group">
<label>{{"EmailAddress" | localize}}</label>
<input class="form-control" type="email" name="emailAddress" [(ngModel)]="student.emailAddress" required maxlength="255" pattern="^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{1,})+$">
</div>
</div>
<div class="modal-footer">
<button [disabled]="saving" type="button" class="btn btn-secondary" (click)="close()">{{"Cancel" | localize}}</button>
<button type="submit" class="btn btn-primary" [disabled]="!personForm.form.valid" [buttonBusy]="saving" [busyText]="l('SavingWithThreeDot' | localize)"><i class="fa fa-save"></i> <span>{{"Save" | localize}}</span></button>
</div>
</form>
</div>
</div>
</div>

This modal is inside ngx-bootstrap library, you can refer here if you need more information.

Most of this code is similar for all modals. The important part is how we binded model to the view using the ngModel directive. As like all components, Angular requires to relate it to a module. We should add it to declarations array of main.module.ts as like shown below:

...previous imports
import { CreateStudentModalComponent } from './learning/create-student-modal.component';
@NgModule({
imports: [
...existing module imports...
],
declarations: [
...
CreateStudentModalComponent
]
})
export class MainModule { }

We need to put a “Create new student” button to the ‘student list page’ to open the modal when clicked to the button. To do that, we made the following changes in learning.component.html:

<div [@routerTransition]>
<div class="kt-content kt-grid__item kt-grid__item--fluid kt-grid kt-grid--hor">
<div class="kt-subheader kt-grid__item">
<div class="kt-container ">
<div class="kt-subheader__main">
<h3 class="kt-subheader__title">
<span>{{"Learning" | localize}}</span>
</h3>
<span class="kt-subheader__separator kt-subheader__separator--v"></span>
<span class="kt-subheader__desc">
{{"EditTenantHeaderInfo" | localize}}
</span>
</div>
<div class="kt-subheader__toolbar">
<div class="kt-subheader__wrapper">
<button class="btn btn-primary" (click)="createStudentModal.show()"><i class="fa fa-plus"></i>
{{"CreateNewStudent" | localize}}</button>
</div>
</div>
</div>
</div>
<div class="kt-container kt-grid__item kt-grid__item--fluid">
<div class="kt-portlet kt-portlet--mobile">
<div class="kt-portlet__body kt-portlet__body--fit">
<h3>{{"AllStudent" | localize}}</h3>
<div *ngFor="let person of student">
<div class="row kt-row--no-padding align-items-center">
<div class="col">
<h4>{{person.name + ' ' + person.surname}}</h4>
<span>{{person.emailAddress}}</span>
</div>
</div>
</div>
</div>
<createStudentModal #createStudentModal (modalSave)="getPeople()"></createStudentModal>
</div>
</div>
</div>
</div>

Made some minor changes in the view; Added a button to open the modal and the createStudentModal component as like another HTML tag (which matches to the selector in the create-student-modal.component.ts).

(9) Authorization for Learning Business

At this point, anyone can enter learning page since no authorization defined. We will define two permission:

  • A permission to enter learning page.
  • A permission to create new student (which is a child permission of first one, as naturally).

(9.1) Permission to enter learning page.

  • Define the permission

Go to AppAuthorizationProvider class in the server side (./Core/Authorization) add a new permission as shown below (you can add just below the dashboard permission):

pages.CreateChildPermission(AppPermissions.Pages_Tenant_Learning, L("Learning"), multiTenancySides: MultiTenancySides.Tenant);

A permission should have a unique name. We define permission names as constant strings in AppPermissions class. It’s a simple constant string:

public const string Pages_Tenant_Learning = "Pages.Tenant.Learning";

Unique name of this permission is “Pages.Tenant.Learning”. While you can set any string (as long as it’s unique), it’s suggested to use that convention. A permission can have a localizable display name: “Learning” here. (See “Adding a New Page” section for more about localization, since it’s very similar). Lastly, we set this as a tenant level permission.

  • Add AbpAuthorize attribute

AbpAuthorize attribute can be used as class level or method level to protect an application service or service method from unauthorized users. Since all server side code is located in StudentAppService class, we can declare a class level attribute as shown below:

[AbpAuthorize(AppPermissions.Pages_Tenant_Student)]
public class StudentAppService : AbpZeroTemplateAppServiceBase,
IStudentAppService
{
//...
}
  • Guard Angular Route

We got an exception about permission. Server did not send the data but we can still enter the page. To prevent it, open main-routing.module.ts and change the route definition like that:

{ path: 'learning', component: LearningComponent, data: { permission: 'Pages.Tenant.Learning' } }

AuthRouteGuard class automatically checks route permission data and prevents entering to the view if specified permission is not granted. Try to click Learning menu!

  • Hide Unauthorized Menu Item

While user can not enter to the page, the menu item still there! We should also hide the Learning menu item. It’s easy, open ./src/app/shared/layout/nav/app-navigation-service.ts and add change Learning menu definition as shown below:

new AppMenuItem("Learning", 'Pages.Tenant.Learning', "flaticon-book", "/app/main/learning")
  • Grant Permission

So, how we can enter the page now? We need to update permission for admin role. But because we are using in Multi-tenant mode and Learning feature is set at tenant level permission, so this permission will be applied for a tenant admin, not host admin.

Now, we have to create a tenant by this screen

Create New Tenant

You can log in tenant account by 2 ways as below

Login from Host Admin.
Login by Default Login Screen

next, we will update the permissions of tenant admin.

Edit Tenant Permission
Set Learning permission

We see that a new permission named “Learning” added to permissions tab. So, we can check it and save the role. After saving, we need to refresh the whole page to refresh permissions for the current user. We could also grant this permission to a specific user. Now, we can enter the Learning page again.

(9.2) Permission to create new student

While a permission for a page is useful and probably always needed, we may want to define additional permissions to perform some specific actions on a page, like creating a new student.

  • Define a permission

Defining a permission is similar (in the ./Core/AuthorizationAppAuthorizationProvider class):

// Remove old line
//pages.CreateChildPermission(AppPermissions.Pages_Tenant_Learning, L("Learning"), multiTenancySides: MultiTenancySides.Tenant);
// New lines
var learning = pages.CreateChildPermission(AppPermissions.Pages_Tenant_Learning, L("Learning"), multiTenancySides: MultiTenancySides.Tenant); learning.CreateChildPermission(AppPermissions.Pages_Tenant_Learning_CreateStudent, L("CreateNewStudent"), multiTenancySides: MultiTenancySides.Tenant);

First permission was defined before. In the second line, we are creating a child permission of first one. Remember to create a constant in AppPermissions class:

public const string Pages_Tenant_Learning_CreateStudent = "Pages.Tenant.Learning.CreateStudent";
  • Add AbpAuthorize Attribute

This time, we’re declaring AbpAuthorize attribute just for CreateStudent method in Student in StudentAppService:

[AbpAuthorize(AppPermissions.Pages_Tenant_Learning_CreateStudent)]
public async Task CreateStudent(CreateStudentInput input)
{
// ..
}
  • Hide Unauthorized Button

If we run the application and try to create a student, we get an authorization error after clicking the save button. But, it’s good to completely hide Create New Student button if we don’t have the permission. It’s very simple:

Open the learning.component.html view and add the permission Pages.Tenant.Learning.CreateStudent condition as shown below:

<button *ngIf="'Pages.Tenant.Learning.CreateStudent' | permission"class="btn btn-primary" (click)="createStudentModal.show()">
<i class="fa fa-plus"></i> {{"CreateNewStudent" | localize}}</button>

In this way, the “Create New Student” button is not rendered in server and user can not see this button.

  • Grant Permission

To see the button again, we can go to role or user manager and grant related permission as shown below:

Set Create New Student permission

(10) Deleting a Student

  • View

We’re changing learning.component.html view to add a delete button (related part is shown here):

...
<h3>{{"AllStudent" | localize}}</h3>
<div class="row kt-row--no-padding align-items-center" *ngFor="let person of student">
<div class="col">
<h4>{{person.name + ' ' + person.surname}}</h4>
<span>{{person.emailAddress}}</span>
</div>
<div class="col kt-align-right">
<button id="deleteStudent" (click)="deleteStudent(person)" title="{{'Delete' | localize}}"
class="btn btn-outline-hover-danger btn-icon"
href="javascript:;">
<i class="fa fa-times"></i>
</button>
</div>
</div>
...
  • Style

We’re using LESS files for styling the components. We created a file named learning.component.less (in learning folder) with an empty content.

/* styles */

And adding the style to the learning.component.ts Component declaration:

@Component({
templateUrl: './learning.component.html',
styleUrls: ['./learning.component.less'],
animations: [appModuleAnimation()]
})

Now, we can now see the buttons, but they don’t work since we haven’t defined the deletePerson method yet.

  • Application Service

Let’s leave the client side and add a DeleteStudent method to the server side. We are adding it to the service interface,IStudentAppService:

Task DeleteStudent(EntityDto input);

EntityDto is a shortcut of ABP if we only get an id value. Implementation (in StudentAppService) is very simple:

[AbpAuthorize(AppPermissions.Pages_Tenant_Learning_DeleteStudent)]
public async Task DeleteStudent(EntityDto input)
{
await _studentRepository.DeleteAsync(input.Id);
}

add constant Pages_Tenant_Learning_DeleteStudent to AppPermissions.

public const string Pages_Tenant_Learning_DeleteStudent = "Pages.Tenant.Learning.DeleteStudent";

add permission to AppAuthorizationProvider

learning.CreateChildPermission(AppPermissions.Pages_Tenant_Learning_CreateStudent, L("DeleteStudent"), multiTenancySides: MultiTenancySides.Tenant);
  • Service Proxy Generation

Since we changed server side services, we should re-generate the client side service proxies via NSwag. Make server side running and use refresh.bat as we did before.

  • Learning Component Script
// import lodash library
import * as _ from 'lodash';
...// add code for delete method
deleteStudent(student: StudentListDto): void {
this.message.confirm(
this.l('AreYouSureToDeleteTheStudent', student.name),
student.name,
isConfirmed => {
if (isConfirmed) {
this._studentService.deleteStudent(student.id).subscribe(() => {
this.notify.info(this.l('SuccessfullyDeleted'));
_.remove(this.student, student);
});
}
}
);
}

It first shows a confirmation message when we click the delete button:

Delete Student
Confirm delete message

If we click Yes, it simply calls deletePerson method of PersonAppService and shows a notification if operation succeed. Also, removes the person from the person array using lodash library.

(11) Filtering Student

We added a search input to learning.component.html view (showing the related part of the code):

<h3>{{"AllStudent" | localize}} ({{student.length}})</h3>
<form autocomplete="off">
<div class="kt-form">
<div class="row align-items-center kt-margin-b-10">
<div class="col-xl-12">
<div class="form-group align-items-center">
<div class="input-group">
<input [(ngModel)]="filter" name="filterText" autoFocus class="form-control" [placeholder]="l('SearchWithThreeDot' | localize)" type="text">
<span class="input-group-btn">
<button (click)="getPeople()" class="btn btn-primary" type="submit"><i class="flaticon-search-1"></i></button>
</span>
</div>
</div>
</div>
</div>
</div>
</form>
...
<div class="row kt-row--no-padding align-items-center" *ngFor="let person of student">
<div class="col">
<h4>{{person.name + ' ' + person.surname}}</h4>
<span>{{person.emailAddress}}</span>
</div>
<div class="col kt-align-right">
<button id="deleteStudent" (click)="deleteStudent(person)" title="{{'Delete' | localize}}"
class="btn btn-outline-hover-danger btn-icon"
href="javascript:;">
<i class="fa fa-times"></i>
</button>
</div>
</div>

We also added currently filtered student count (student.length) in the header. Since we have already defined and used the filter property in learning.component.ts and implemented in the server side, this new code immediately works.

(12) Edit Mode For People

First of all, we create the necessary DTOs to transfer student’s id, name, surname and e-mail. We can optionally configure auto-mapper, but this is not necessary because all properties match automatically. Then we create the functions in StudentAppService for editing people:

StudentAppService

[AbpAuthorize(AppPermissions.Pages_Tenant_Learning_EditStudent)]
public async Task<GetStudentForEditOutput> GetStudentForEdit(GetStudentForEditInput input)
{
var student = await _studentRepository.GetAsync(input.Id);
return ObjectMapper.Map<GetStudentForEditOutput>(student);
}
[AbpAuthorize(AppPermissions.Pages_Tenant_Learning_EditStudent)]
public async Task EditStudent(EditStudentInput input)
{
var student = await _studentRepository.GetAsync(input.Id);
student.Name = input.Name;
student.Surname = input.Surname;
student.EmailAddress = input.EmailAddress;
await _studentRepository.UpdateAsync(student);
}

IStudentAppService

Task<GetStudentForEditOutput> GetStudentForEdit(GetStudentForEditInput input);
Task EditStudent(EditStudentInput input);

Create new EditStudentInput class in ./Application/Shared/Learning/Dto

public class EditStudentInput
{
public int Id { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
public string EmailAddress { get; set; }
}

Create new GetStudentForEditInput

public int Id { get; set; }

Create new GetStudentForEditOutput

public class GetStudentForEditOutput
{
public int Id { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
public string EmailAddress { get; set; }
}

Then we add configuration for AutoMapper into CustomDtoMapper.cs like below:

configuration.CreateMap<Student, GetStudentForEditOutput>();

Add to AppPermissions

public const string Pages_Tenant_Learning_EditStudent = "Pages.Tenant.Learning.EditStudent";

Add to AppAuthorizationProvider

learning.CreateChildPermission(AppPermissions.Pages_Tenant_Learning_EditStudent, L("EditStudent"), multiTenancySides: MultiTenancySides.Tenant);
  • Service Proxy Generation

Since we changed server side services, we should re-generate the client side service proxies via NSwag. Make server side running and use refresh.bat as we did before.

  • View

Create edit-student-modal.component.html:

<div bsModal #modal="bs-modal" (onShown)="onShown()" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="modal" aria-hidden="true" [config]="{backdrop: 'static'}">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<form *ngIf="active" #studentForm="ngForm" novalidate (ngSubmit)="save()">
<div class="modal-header">
<h4 class="modal-title">
<span>{{"EditStudent" | localize}}</span>
</h4>
<button type="button" class="close" (click)="close()" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">

<div class="form-group">
<label>{{"Name" | localize}}</label>
<input #nameInput class="form-control" type="text" name="name" [(ngModel)]="student.name" required maxlength="32">
</div>

<div class="form-group">
<label>{{"Surname" | localize}}</label>
<input class="form-control" type="text" name="surname" [(ngModel)]="student.surname" required maxlength="32">
</div>

<div class="form-group">
<label>{{"EmailAddress" | localize}}</label>
<input class="form-control" type="email" name="emailAddress" [(ngModel)]="student.emailAddress" required maxlength="255" pattern="^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{1,})+$">
</div>

</div>
<div class="modal-footer">
<button [disabled]="saving" type="button" class="btn btn-secondary" (click)="close()">{{"Cancel" | localize}}</button>
<button type="submit" class="btn btn-primary" [disabled]="!studentForm.form.valid" [buttonBusy]="saving" [busyText]="l('SavingWithThreeDot' | localize)"><i class="fa fa-save"></i> <span>{{"Save" | localize}}</span></button>
</div>
</form>
</div>
</div>
</div>

Add those lines to learning.component.html:

...
<button *ngIf="'Pages.Tenant.Learning.EditStudent' | permission"
(click)="editStudentModal.show(person.id)" title="{{'EditStudent' | localize}}"
class="btn btn-outline-hover-success btn-icon">
<i class="fa fa-edit"></i>
</button>
<button id="deleteStudent" (click)="deleteStudent(person)" title="{{'Delete' | localize}}"
class="btn btn-outline-hover-danger btn-icon"
href="javascript:;">
<i class="fa fa-times"></i>
</button>
...
<createStudentModal #createStudentModal (modalSave)="getPeople()"></createStudentModal>
<editStudentModal #editStudentModal (modalSave)="getPeople()"></editStudentModal>
  • Controller

Create edit-student-modal.component.ts:

import { Component, ViewChild, Injector, ElementRef, Output, EventEmitter } from '@angular/core';
import { ModalDirective } from 'ngx-bootstrap';
import { StudentServiceProxy, EditStudentInput } from '@shared/service-proxies/service-proxies';
import { AppComponentBase } from '@shared/common/app-component-base';
@Component({
selector: 'editStudentModal',
templateUrl: './edit-student-modal.component.html'
})
export class EditStudentModalComponent extends AppComponentBase {
@Output() modalSave: EventEmitter<any> = new EventEmitter<any>();@ViewChild('modal' , { static: false }) modal: ModalDirective;
@ViewChild('nameInput' , { static: false }) nameInput: ElementRef;
student: EditStudentInput = new EditStudentInput();active: boolean = false;
saving: boolean = false;
constructor(
injector: Injector,
private _studentService: StudentServiceProxy
) {
super(injector);
}
show(studentId): void {
this.active = true;
this._studentService.getStudentForEdit(studentId).subscribe((result)=> {
this.student = result;
this.modal.show();
});
}onShown(): void {
// this.nameInput.nativeElement.focus();
}
save(): void {
this.saving = true;
this._studentService.editPerson(this.student)
.subscribe(() => {
this.notify.info(this.l('SavedSuccessfully'));
this.close();
this.modalSave.emit(this.student);
});
this.saving = false;
}
close(): void {
this.modal.hide();
this.active = false;
}
}

Add those lines to main.module.ts:

import { EditStudentModalComponent } from './learning/edit-student-modal.component';// Other Code lines...declarations: [
...
CreateStudentModalComponent,
EditStudentModalComponent
]
// Other Code lines...

That’s it! All done! And this is our result

Final Result

References:

--

--

Quang Trong VU
Old Dev
Editor for

Software Developer — Life’s a journey — Studying and Sharing