.NET Core  

Validations with DataAnnotations and User Creation [GamesCatalog] 19

Previous part: Creating a WebApi Project in .NET 9 [GamesCatalog] 18

Step 1. Let's work on user creation. To start, we'll create the user repository.

Step 1.1. Create the CreateAsync and GetByEmailAsync functions and generate the interface:

using Microsoft.EntityFrameworkCore;
using Models.DTOs;

namespace Repos;

public class UserRepo(IDbContextFactory<DbCtx> DbCtx) : IUserRepo
{
    public async Task CreateAsync(UserDTO user)
    {
        using var context = DbCtx.CreateDbContext();

        await context.User.AddAsync(user);

        await context.SaveChangesAsync();
    }

    public async Task<UserDTO?> GetByEmailAsync(string email)
    {
        using var context = DbCtx.CreateDbContext();
        return await context.User.FirstOrDefaultAsync(x => x.Email.Equals(email));
    }
}

Step 2. Let's create the models that we'll use for user creation.

We'll have two folders: Requests and Responses. Inside Requests, we'll create a folder for the user models. The user models will be divided into separate parts that can eventually be used individually:

Step 2.1. ReqBaseModel.cs will contain the function responsible for validating the model fields:

Code for ReqBaseModel.cs

using System.ComponentModel.DataAnnotations;

namespace Models.Reqs;

public record ReqBaseModel
{
    public string? Validate()
    {
        List<ValidationResult> validationResult = [];

        Validator.TryValidateObject(this, new ValidationContext(this), validationResult, true);

        if (validationResult.Count > 0) return validationResult.First().ErrorMessage;
        else return null;
    }
}

Step 2.2. Inside the User folder, ReqUserEmail.cs will contain the validation Annotations for the email field:

using System.ComponentModel.DataAnnotations;

namespace Models.Reqs.User;

public record ReqUserEmail : ReqBaseModel
{
    [Display(Name = "Email")]
    [DataType(DataType.EmailAddress)]
    [Required(ErrorMessage = "Email is required")]
    [RegularExpression(@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$", ErrorMessage = "Invalid Email")]
    [StringLength(250, MinimumLength = 4)]
    public required string Email { get; init; }
}

Step 2.3. ReqUserSession.cs will contain the password validation and inherit from ReqUserEmail.

using System.ComponentModel.DataAnnotations;

namespace Models.Reqs.User;

public record ReqUserSession : ReqUserEmail
{
    [Display(Name = "Password")]
    [DataType(DataType.Password)]
    [Required(ErrorMessage = "Password is required")]
    [StringLength(30, MinimumLength = 4)]
    public required string Password { get; init; }
}

Step 2.4. ReqUser.cs will inherit from ReqUserSession.cs and include the validation for the name field, which will be optional.

using System.ComponentModel.DataAnnotations;

namespace Models.Reqs.User;

public record ReqUser : ReqUserSession
{
    [StringLength(150)]
    public string? Name { get; init; } = null;
}

Step 2.5. In the Responses folder, we'll create BaseResp.cs to handle the service responses:

namespace Models.Resps;

public record BaseResp
{
    public bool Success { get; set; } = true;

    public object? Content { get; init; }

    public Error? Error { get; init; } = null;

    public BaseResp(object? content, string? errorMessage = null)
    {
        if (!string.IsNullOrEmpty(errorMessage))
        {
            Success = false;
            Content = null;

            if (!string.IsNullOrEmpty(errorMessage))
                Error = new Error { Message = errorMessage };
        }
        else Content = content;
    }
}

public record Error
{
    public required string Message { get; init; }
}

Step 2.6. We'll also create the model ResUser.cs:

namespace Models.Resps;

public record ResUser
{
    public int Id { get; init; }

    public string? Name { get; init; }

    public string? Email { get; init; }

    public DateTime CreatedAt { get; init; }
}

Step 3. Now let's create UserService.cs:

Code

using Models.DTOs;
using Models.Reqs.User;
using Models.Resps;
using Repos;

namespace Services
{
    public class UserService(IUserRepo userRepo) : IUserService
    {
        public async Task<BaseResp> CreateAsync(ReqUser reqUser)
        {
            string? validateError = reqUser.Validate();
            if (!string.IsNullOrEmpty(validateError)) return new BaseResp(null, validateError);

            UserDTO user = new()
            {
                Name = reqUser.Name,
                Email = reqUser.Email,
                Password = reqUser.Password,
                CreatedAt = DateTime.Now
            };

            string? existingUserMessage = await ValidateExistingUserAsync(user);
            if (existingUserMessage != null) { return new BaseResp(null, existingUserMessage); }

            await userRepo.CreateAsync(user);

            ResUser? resUser = new()
            {
                Id = user.Id,
                Name = user.Name,
                Email = user.Email,
                CreatedAt = user.CreatedAt
            };

            return new BaseResp(resUser);
        }

        protected async Task<string?> ValidateExistingUserAsync(UserDTO user)
        {
            UserDTO? userResp = await userRepo.GetByEmailAsync(user.Email);

            if (userResp is not null && userResp.Email.Equals(user.Email))
                return "User Email already exists.";

            return null;
        }
    }
}

In this code, inside CreateAsync(), we validate the user data using DataAnnotations. With ValidateExistingUserAsync(), we check if a user with the same email already exists, and if not, the user is registered. Extract the interface.

Step 4. Let's create a BaseController and a UserController.

Code for BaseController

using Microsoft.AspNetCore.Mvc;
using Models.Resps;

namespace GamesCatalogAPI.Controllers
{
    public class BaseController : Controller
    {
        protected IActionResult BuildResponse(BaseResp baseResp) =>
            (!string.IsNullOrEmpty(baseResp.Error?.Message)) ?
            BadRequest(baseResp.Error.Message) :
            Ok(baseResp.Content);
    }
}

Code for UserController

using Microsoft.AspNetCore.Mvc;
using Models.Reqs.User;
using Services;

namespace GamesCatalogAPI.Controllers
{
    [Route("[Controller]")]
    [ApiController]
    public class UserController(IUserService userService) : BaseController
    {
        [Route("")]
        [HttpPost]
        public async Task<IActionResult> SignUp(ReqUser reqUser) => BuildResponse(await userService.CreateAsync(reqUser));
    }
}

Step 5. In the Program.cs, let's add UserService and UserRepo to the Dependency Injection (DI) container:

Code

#region Repos

builder.Services.AddScoped<IUserRepo, UserRepo>();

#endregion

#region Servs

builder.Services.AddScoped<IUserService, UserService>();

#endregion

Step 6. Update the GamesCatalogAPI.http file to insert a test for the user creation POST request:

Code

@GamesCatalogAPI_HostAddress = http://localhost:5048

POST {{GamesCatalogAPI_HostAddress}}/User/
Content-Type: application/json
{
  "name": "test",
  "email": "[email protected]",
  "password": "pass123"
}
###

Step 7. When running the system and sending the request, we receive a successful response.

Formatted raw Header

Step 8. When sending the request again, we receive a response indicating that the email is already registered.

Formatted raw Header

In the next step, we will encrypt the email and generate a token to manage user access.

Code on git: GamesCatalogBackEnd