This application dynamically selects the database based on the user’s login. During startup, it may take additional time due to the execution of database seeders. Since the database is determined per user, the application uses HTML templates for generating reports. These templates are processed with Scriban and exported to PDF using DinkToPdf, with the output stored in a temporary directory.
Because report generation and previewing are manual processes, the application also includes a scheduler that periodically cleans up the temporary directory to manage storage efficiently.
The project is structured as follows:
SafeGeneral-ABB/
├── .gitattributes
├── .gitignore
├── .idea/
├── ABB.Api/
├── ABB.Application/
├── ABB.Domain/
├── ABB.Infrastructure/
├── ABB.Web/
├── ABB.sln
├── ABB.sln.DotSettings.user
├── Include/
└── global.json
ABB.Api: Contains the API layer of the application.ABB.Application: Contains the application logic.ABB.Domain: Contains the domain models and entities.ABB.Infrastructure: Contains the infrastructure-related code, such as database connections and external services.ABB.Web: Contains the web application.ABB.sln: The solution file for the project.
- Net Core 3.1
This section provides examples of how to use MediatR, AutoMapper, and FluentValidation within the project.
MediatR is used for implementing the Mediator pattern, enabling decoupled communication between different parts of the application.
Let's say you have a command to create a new user.
-
Define the Command:
public class CreateUserCommand : IRequest<User> { public string Username { get; set; } public string Email { get; set; } }
-
Create the Handler:
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, User> { private readonly IUserRepository _userRepository; public CreateUserCommandHandler(IUserRepository userRepository) { _userRepository = userRepository; } public async Task<User> Handle(CreateUserCommand request, CancellationToken cancellationToken) { var user = new User { Username = request.Username, Email = request.Email }; await _userRepository.AddAsync(user); return user; } }
-
Dispatch the Command:
public async Task<IActionResult> CreateUser(string username, string email, IMediator mediator) { var command = new CreateUserCommand { Username = username, Email = email }; var user = await mediator.Send(command); return Ok(user); }
Let's say you have a query to get a user by ID.
-
Define the Query:
public class GetUserQuery : IRequest<User> { public int Id { get; set; } }
-
Create the Handler:
public class GetUserQueryHandler : IRequestHandler<GetUserQuery, User> { private readonly IUserRepository _userRepository; public GetUserQueryHandler(IUserRepository userRepository) { _userRepository = userRepository; } public async Task<User> Handle(GetUserQuery request, CancellationToken cancellationToken) { var user = await _userRepository.GetByIdAsync(request.Id); return user; } }
-
Dispatch the Query:
public async Task<IActionResult> GetUser(int id, IMediator mediator) { var query = new GetUserQuery { Id = id }; var user = await mediator.Send(query); return Ok(user); }
AutoMapper is used for mapping objects from one type to another. In this project, AutoMapper configurations are defined directly within the ViewModels, DTOs, Commands, or Queries.
-
Define the DTO (with Mapping Configuration):
public class UserDto : IMapFrom<User> { public int Id { get; set; } public string Username { get; set; } public string Email { get; set; } public void Mapping(Profile profile) { profile.CreateMap<User, UserDto>(); } }
Note: The
Mappingmethod is an example. You might use a different approach to configure the mapping within your DTO/ViewModel. The key is that the mapping logic resides within the class itself. -
Use the Mapper:
public async Task<IActionResult> GetUser(int id, IMediator mediator, IMapper mapper) { var query = new GetUserQuery { Id = id }; var user = await mediator.Send(query); var userDto = mapper.Map<UserDto>(user); return Ok(userDto); }
FluentValidation is used for request validation, and the validation logic is applied directly within the controller actions.
-
Define a Validator:
public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand> { public CreateUserCommandValidator() { RuleFor(x => x.Username).NotEmpty().WithMessage("Username is required."); RuleFor(x => x.Email).NotEmpty().WithMessage("Email is required.").EmailAddress(); } }
-
Use on Controller:
public async Task<IActionResult> CreateUser([FromBody] UserViewModel model) { try { var command = Mapper.Map<CreateUserCommand>(model); command.DatabaseName = Request.Cookies["DatabaseValue"]; await Mediator.Send(command); return Json(new { Result = "OK", Message = Constant.DataDisimpan}); } catch (ValidationException ex) { ModelState.AddModelErrors(ex); } catch (Exception ex) { return Json(new { Result = "ERROR", Message = ex.Message }); } return PartialView(model); }