Adding Profile Fields
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.
Backend Changes
We’ll start off by updating the backend by changing in layers from entity model to DTOs and then services. Most of the backend 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; } = string.Empty; public required string LastName { get; set; } = string.Empty; ...
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
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; } ...
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, ...
-
Update fields for the first and last name to the
ToCreate
method.public static ApplicationUser ToCreate(this RegisterRequestDto dto, bool twoFactorEnabled) { var user = new ApplicationUser() { FirstName = dto.FirstName, LastName = dto.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 A Migration
-
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.
Update Tests
For the sake of brevity, updates to the test project are not covered in this article. However, updating them should be straightforward as the API surface area limits changes exposed to the outside to the DTOs with new fields, such as RegisterRequestDto
. It’s also a good practice to add/update tests for the new fields and functionality.
Additional Backend Changes
Because all profile manipulation is handled through DTOs and extension methods there is no need to make any other changes on the backend. 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.
Frontend Changes
The frontend is also divided into areas that map directly to the backend 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 frontend is contained in the lightnap-ng
project.
Updating the Registration Frontend
- Open
app/core/backend-api/identity/dtos/identity/request/register-request-dto.ts
. This is the model that maps to the backendRegisterRequestDto
. -
Add fields for the first and last names.
export interface RegisterRequest { firstName: string; lastName: string; ...
- Open
app/pages/identity/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/pages/identity/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="text-xl">First Name</label> <input id="firstName" type="text" placeholder="First Name" pInputText formControlName="firstName" /> <label for="lastName" class="text-xl">Last Name</label> <input id="lastName" type="text" placeholder="Last Name" pInputText formControlName="lastName" /> ...
Updating the Profile Frontend
- Open
app/core/backend-api/profile/response/profile-dto.ts
. This is the model that maps to the backendProfileDto
. -
Add fields for the first and last names.
export interface Profile { firstName: string; lastName: string; ...
- Open
app/core/backend-api/profile/request/update-profile-request-dto.ts
. This is the model that maps to the backendUpdateProfileDto
. -
Add fields for the first and last names.
export interface UpdateProfileRequest { firstName: string; lastName: string; ...
- Open
app/pages/profile/index/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 IndexComponent { ... 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(profile => { // Set form values. this.form.setValue({ firstName: profile.firstName, lastName: profile.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/pages/profile/index/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-col gap-1"> <label for="firstName" class="font-semibold">First Name</label> <input id="firstName" type="text" pInputText formControlName="firstName" class="w-full mb-2" /> </div> <label for="lastName" class="font-semibold">Last Name</label> <input id="lastName" type="text" pInputText formControlName="lastName" class="w-full mb-2" /> ...
Updating the User Functionality
While the profile functionality covers a given user and their own details, there is also a user concept used throughout LightNap that covers how the data for a given account is rendered based on user permission. By default, the backend DTOs for this are:
PublicUserDto
: The minimum details accessible to a user who is not logged in.PrivilegedUserDto
: An extension ofPublicUserDto
that extends with fields available to privileged users. In the default implementation this applies to any authenticated user.AdminUserDto
: An extension of thePrivilegedUserDto
that includes the full set of fields available to users in theAdministrator
role.
Since these DTOs inherit from one another, each derived class automatically includes the properties of its base class. For example, if you want the first and last name properties to only be visible to privileged and administrator users, add them to PrivilegedUserDto
. The same goes for the search DTOs like PublicUsersSearchRequestDto
and so on. These are separated so that you can easily use a different set of search parameters from what is available in the response DTO.
If you’d like to continue the exercise to implement these new properties for user functionality throughout the app, the general steps are:
- Update the backend DTOs to reflect the data available for retrieving and searching users.
- Update
Extensions/ApplicationUserExtensions.cs
to populate the fields of the various DTO mappings. - Update
UsersService.SearchUsersAsync
inUsers/Services/UsersService.cs
to implement search on filters supported at the appropriate privilege level. - Update the frontend DTOs in
core/backend-api/dtos/users
to reflect the changes made to the backend DTOs. - Update page components in
pages/admin
to render the new user fields in the appropriate places.