Welcome to the sixteenth episode of my course “Becoming a software developer” in which we will implement the login endpoint in our API, discuss the caching mechanism and how to initialize the application with basic data.
All of the materials including videos and sample projects can be downloaded from here.
The source code repository is being hosted on GitHub.
Scope
- Login
- Seed
Abstract
Login
Once we have JWT authentication in place, the final step is to allow the users to actually get the valid token via exposed login endpoint. In order to do so, we can define a simple Login command along with its handler, like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public class Login : ICommand { public Guid TokenId { get; set; } public string Email { get; set; } public string Password { get; set; } } public class LoginHandler : ICommandHandler<Login> { private readonly IUserService _userService; private readonly IJwtHandler _jwtHandler; private readonly IMemoryCache _cache; public LoginHandler(IUserService userService, IJwtHandler jwtHandler, IMemoryCache cache) { _userService = userService; _jwtHandler = jwtHandler; _cache = cache; } public async Task HandleAsync(Login command) { await _userService.LoginAsync(command.Email, command.Password); var user = await _userService.GetAsync(command.Email); var jwt = _jwtHandler.CreateToken(user.Id, user.Role); _cache.SetJwt(command.TokenId, jwt); } } |
As you might have already noticed, I’m using the IMemoryCache which allows using caching mechanism in our application. Whenever you want to cache something, always think in terms how big the data would be (server has a limited memory), does it change too often or not and, is it a costly to e.g. fetch it from a remote resource (database, service) and is it going to be accessed by the end users quite often.
Back to the point, eventually, we can create a controller that will consume and handle the Login command. You might be wondering what do we need caching here for? Since we’re following the CQS (Command & Query Separation) pattern, our command handlers do not return any values. Thus, we need to store our token in some place (it could be e.g. a real database, but in this case, it’s just a memory) in order to fetch it and return to the user.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class LoginController : ApiControllerBase { private readonly IMemoryCache _cache; public LoginController(ICommandDispatcher commandDispatcher, IMemoryCache cache) : base(commandDispatcher) { _cache = cache; } [HttpPost] public async Task<IActionResult> Post([FromBody]Login command) { command.TokenId = Guid.NewGuid(); await DispatchAsync(command); var jwt = _cache.GetJwt(command.TokenId); return Json(jwt); } } |
Seed
Quite often, we’d like to have some initial data, so we can easily browse or test the API. We can delegate such task to a specialized service that usually exposes a method called Seed().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
public interface IDataInitializer : IService { Task SeedAsync(); } public class DataInitializer : IDataInitializer { private readonly IUserService _userService; private readonly IDriverService _driverService; private readonly IDriverRouteService _driverRouteService; private readonly ILogger<DataInitializer> _logger; public DataInitializer(IUserService userService, IDriverService driverService, IDriverRouteService driverRouteService, ILogger<DataInitializer> logger) { _userService = userService; _driverService = driverService; _driverRouteService = driverRouteService; _logger = logger; } public async Task SeedAsync() { _logger.LogTrace("Initializing data..."); var tasks = new List<Task>(); for(var i=1; i<=10; i++) { var userId = Guid.NewGuid(); var username = $"user{i}"; tasks.Add(_userService.RegisterAsync(userId, $"user{i}@test.com", username, "secret", "user")); _logger.LogTrace($"Adding user: '{username}'."); tasks.Add(_driverService.CreateAsync(userId)); tasks.Add(_driverService.SetVehicle(userId, "BMW", "i8")); tasks.Add(_driverRouteService.AddAsync(userId, "Default route", 1,1,2,2)); tasks.Add(_driverRouteService.AddAsync(userId, "Job route", 3,3,5,5)); _logger.LogTrace($"Adding driver for: '{username}'."); } for(var i=1; i<=3; i++) { var userId = Guid.NewGuid(); var username = $"admin{i}"; _logger.LogTrace($"Adding admin: '{username}'."); tasks.Add(_userService.RegisterAsync(userId, $"admin{i}@test.com", username, "secret", "admin")); } await Task.WhenAll(tasks); _logger.LogTrace("Data was initialized."); } } |
We may also go one step further and include some application settings (e.g. a boolean flag) in order to control whether the application should initialize the data or not.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class GeneralSettings { public string Name { get; set; } public bool SeedData { get; set; } } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime appLifetime) { //The remaining configuration of our API. var generalSettings = app.ApplicationServices.GetService<GeneralSettings>(); if(generalSettings.SeedData) { var dataInitializer = app.ApplicationServices.GetService<IDataInitializer>(); dataInitializer.SeedAsync(); } } |
Next
In the next episode we will keep on implementing our business logic and findining out both, the boundaries and responsibilities of the different services and how they should interact with each other.
Nice episode. However I don’t think that injecting logger into each class is a good practice. In my opinion it’d be better to use simply private static field instead of defining additional constructor dependency.
I do agree, personally I use NLog and private static field as well just wanted to show the easiest example here.
Awesome episode. Loved it.
loved reading your post. you view London School of economics Details from here.