The default LightNap profile isn’t very interesting. It just has some default fields and timestamps. Let’s take a look at how it can be extended to have new fields for first and last names.
Back-End Changes
We’ll start off by updating the back-end by changing in layers from entity model to DTOs and then services. Most of the back-end work that needs to be done to change the data model happens in the LightNap.Core
project.
Updating the Entity
- Open
Data/Entities/ApplicationUser.cs
. This is the entity that represents a user. -
Add properties for the first and last name.
public class ApplicationUser : IdentityUser { public required string FirstName { get; set; } public required string LastName { get; set; } ...
-
Update the constructor to set default values for these fields. Alternatively you could allow these to be nullable or required as constructor parameters. This tutorial will keep things simple and just default them to values.
[SetsRequiredMembers] public ApplicationUser(string userName, string email, bool twoFactorEnabled) { this.FirstName = "DefaultFirst"; this.LastName = "DefaultLast"; ...
-
Add an Entity Framework migration and update the database.
It’s recommended to use the in-memory data provider while working out the details of an entity model update, if feasible. Then a single migration can be created and applied once the design is finalized.
Updating the Data Transfer Objects (DTOs)
Almost all access to the ApplicationUser
class is restricted to the services exposed by the project. As a result, there are DTOs that need to be updated.
- Open
Profile/Dto/Response/ProfileDto.cs
. This is the DTO used in responses to logged-in user requests for their own profile. -
Add fields for the first and last name.
public class ProfileDto { public required string FirstName { get; set; } public required string LastName { get; set; } ...
- Open
Profile/Dto/Request/UpdateProfileDto.cs
. This is the DTO used by users requesting updates to their profile. -
Add fields for the first and last name.
public class UpdateProfileDto { public required string FirstName { get; set; } public required string LastName { get; set; } ...
- Open
Administrator/Dto/Response/AdminUserDto.cs
. This is the DTO used in responses to administrator requests for users. -
Add fields for the first and last name.
public class AdminUserDto { public required string FirstName { get; set; } public required string LastName { get; set; } ...
- Open
Administrator/Dto/Request/UpdateAdminUserDto.cs
. This is the DTO used by administrators requesting updates to a user. -
Add fields for the first and last name.
public class UpdateAdminUserDto { public required string FirstName { get; set; } public required string LastName { get; set; } ...
If there were other DTOs for ApplicationUser
, such as those used by the PublicService
or UserService
services, then those would need to be updated as well.
Updating the Extension Method Mappings
There is no direct mapping relationship between the ApplicationUser
class and its related DTOs. That mapping is all performed by external extension methods added to ApplicationUser
. Those methods need to be updated to account for the new fields.
- Open
Extensions/ApplicationUserExtensions.cs
. This class contains all extension methods for convertingApplicationUser
instances to DTOs and for applying changes from DTOs to anApplicationUser
instance. -
Add fields for the first and last name to the
ToLoggedInUserDto
method.public static ProfileDto ToLoggedInUserDto(this ApplicationUser user) { return new ProfileDto() { FirstName = user.FirstName, LastName = user.LastName, ...
-
Add fields for the first and last name to the
UpdateLoggedInUser
method.public static void UpdateLoggedInUser(this ApplicationUser user, UpdateProfileDto dto) { user.FirstName = dto.FirstName; user.LastName = dto.LastName; ...
-
Add fields for the first and last name to the
ToAdminUserDto
method.public static AdminUserDto ToAdminUserDto(this ApplicationUser user) { ... return new AdminUserDto() { FirstName = user.FirstName, LastName = user.LastName, ...
-
Add fields for the first and last name to the
UpdateAdminUserDto
method.public static void UpdateAdminUserDto(this ApplicationUser user, UpdateAdminUserDto dto) { user.FirstName = dto.FirstName; user.LastName = dto.LastName; ...
Updating the Registration Back-End
In this scenario we will assume that the user also needs to provide these fields when registering a new account.
- Open
Identity/Dto/Request/RegisterRequestDto.cs
. This is the DTO submitted by users registering an account on the site. -
Add fields for the first and last name.
public class RegisterRequestDto { public required string FirstName { get; set; } public required string LastName { get; set; } ...
-
Open
Identity/Services/IdentityService.cs
. This is the service that fulfills all identity-related functionality. -
Update the
RegisterAsync
method to set the fields on a newApplicationUser
.public async Task<ApiResponseDto<LoginResultDto>> RegisterAsync(RegisterRequestDto requestDto) { ... ApplicationUser user = new(requestDto.UserName, requestDto.Email, applicationSettings.Value.RequireTwoFactorForNewUsers); user.FirstName = requestDto.FirstName; user.LastName = requestDto.LastName; ...
Updating the Administrator Search Users Back-End
- Open
Administrator/Dto/Request/SearchAdminUsersDto.cs
. This is the DTO used by administrators to search the membership across supported fields. -
Add fields for the first and last name.
public class SearchAdminUsersDto { public string? FirstName { get; set; } public string? LastName { get; set; } ...
- Open
Administrator/Services/AdministratorService.cs
. This is the service that fulfills all administrator-related functionality. -
Update the
SearchUsersAsync
method to apply the name parameters for exact matches, if provided.public async Task<ApiResponseDto<PagedResponse<AdminUserDto>>> SearchUsersAsync(SearchUsersRequestDto requestDto) { IQueryable<ApplicationUser> query = db.Users.AsQueryable(); if (!string.IsNullOrWhiteSpace(requestDto.FirstName)) { query = query.Where(user => user.FirstName == user.FirstName); } if (!string.IsNullOrWhiteSpace(requestDto.LastName)) { query = query.Where(user => user.LastName == user.LastName); } ...
Additional Back-End Changes
Because all profile manipulation is handled through DTOs and extension methods there is no need to make any other changes on the back-end. The data will now flow from the REST API as request DTOs that validate input values as required.
If there is a need to enforce additional restrictions, such as length ranges, that can be done via attributes on the request DTOs (see RegisterRequestDto
for examples on how this can be done). Otherwise all incoming request DTOs are passed by the controllers to their underlying services that call ApplicationUser
extension methods to get or update database data. However, if there is a need to apply further rules or transformations, that can be done within the service methods.
Front-End Changes
The front-end is also divided into areas that map directly to the back-end areas including profile, administrator, and identity. We will approach them area by area so that a full data flow from API to component can be completed before moving to the next. Everything front-end is contained in the lightnap-ng
project.
Updating the Registration Front-End
- Open
app/identity/models/request/register-request.ts
. This is the model that maps to the back-endRegisterRequestDto
. -
Add fields for the first and last names.
export interface RegisterRequest { firstName: string; lastName: string; ...
- Open
app/identity/components/pages/register.component.ts
. This is the code for the page where users register. -
Add fields for the first and last names to the form. This will allow easy binding in the reactive form markup.
export class RegisterComponent { ... form = this.#fb.nonNullable.group({ firstName: this.#fb.control("", [Validators.required]), lastName: this.#fb.control("", [Validators.required]), ...
-
Update the
#identityService.register()
parameter with fields for the names.register() { ... this.#identityService.register({ firstName: this.form.value.firstName, lastName: this.form.value.lastName, ...
- Open
app/identity/components/pages/register.component.html
. This is the markup for the page where users register. -
Add input fields for the names before the password input.
... <label for="firstName" class="block text-900 text-xl font-medium mb-2">First Name</label> <input id="firstName" type="text" placeholder="First Name" pInputText formControlName="firstName" class="w-full md:w-30rem mb-5" style="padding: 1rem" /> <label for="lastName" class="block text-900 text-xl font-medium mb-2">Last Name</label> <input id="lastName" type="text" placeholder="Last Name" pInputText formControlName="lastName" class="w-full md:w-30rem mb-5" style="padding: 1rem" /> <label for="password" class="block text-900 font-medium text-xl mb-2">Password</label> ...
Updating the Profile Front-End
- Open
app/profile/models/response/profile.ts
. This is the model that maps to the back-endProfileDto
. -
Add fields for the first and last names.
export interface Profile { firstName: string; lastName: string; ...
- Open
app/profile/models/request/update-profile-request.ts
. This is the model that maps to the back-endUpdateProfileDto
. -
Add fields for the first and last names.
export interface UpdateProfileRequest { firstName: string; lastName: string; ...
- Open
app/profile/components/pages/index.component.ts
. This is the code for the page users see when they visit their profile. It includes a stub for a profile update form, but there are no fields by default. -
Add fields for the first and last names to the form. This will allow easy binding in the reactive form markup.
export class RegisterComponent { ... form = this.#fb.group({ firstName: this.#fb.control("", [Validators.required]), lastName: this.#fb.control("", [Validators.required]), ...
-
Update the
getProfile
tap
to update the values in the form after the profile has loaded.profile$ = this.#profileService.getProfile().pipe( tap(response => { if (!response.result) return; // Set form values. this.form.setValue({ firstName: response.result.firstName, lastName: response.result.lastName, ...
-
Update the call to the
#profileService.updateProfile
parameter to include the new fields.updateProfile() { ... this.#profileService.updateProfile({ firstName: this.form.value.firstName, lastName: this.form.value.lastName ...
- Open
app/profile/components/pages/index.component.html
. This is the markup for the page users see when they visit their profile. It also includes a stub for a profile update form, but there are no fields by default. -
Update the body of the form with some markup for the new fields.
<form [formGroup]="form" (ngSubmit)="updateProfile()" autocomplete="off"> <div class="flex flex-column w-20rem"> <label for="firstName" class="font-semibold mb-2">First Name</label> <input id="firstName" type="text" pInputText formControlName="firstName" class="w-full mb-2" style="padding: 1rem" /> <label for="lastName" class="font-semibold mb-2">Last Name</label> <input id="lastName" type="text" pInputText formControlName="lastName" class="w-full mb-2" style="padding: 1rem" /> ...
Updating the Administrator Front-End
Updating the Administrator functionality is similar to the Profile work we just completed, so we’ll skip most of it for brevity. But to illustrate the consistency of the areas, here are the general steps to add it:
- Update
app/administrator/models/response/admin-user.ts
with the new fields. This is the model that maps to the back-endAdminUserDto
. - Update
app/administrator/models/request/update-admin-user-request.ts
with the new fields. This is the model that maps to the back-endUpdateAdminUserDto
. - Update
app/administrator/models/request/search-admin-users-request.ts
with the new fields. This is the model that maps to the back-endSearchAdminUsersDto
. - Update
users.component.ts
andusers.component.html
fromapp/administrator/components/pages/users
with form fields and markup to search users and/or include the new fields in the results table. - Update
user.component.ts
anduser.component.html
fromapp/administrator/components/pages/user
with form fields and markup to view and update user fields.