From ebf9cd36d7451539435f77ee5d9d4fb7fc984d4a Mon Sep 17 00:00:00 2001 From: alex289 Date: Thu, 31 Aug 2023 20:13:36 +0200 Subject: [PATCH 1/3] feat: Add background service --- .../SetInactiveUsersService.cs | 69 +++++++++++++++++++ CleanArchitecture.Api/Program.cs | 3 + .../LoginUser/LoginUserCommandHandler.cs | 8 +++ CleanArchitecture.Domain/Entities/User.cs | 21 +++++- CleanArchitecture.Domain/Enums/UserStatus.cs | 7 ++ 5 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 CleanArchitecture.Api/BackgroundServices/SetInactiveUsersService.cs create mode 100644 CleanArchitecture.Domain/Enums/UserStatus.cs diff --git a/CleanArchitecture.Api/BackgroundServices/SetInactiveUsersService.cs b/CleanArchitecture.Api/BackgroundServices/SetInactiveUsersService.cs new file mode 100644 index 0000000..4988645 --- /dev/null +++ b/CleanArchitecture.Api/BackgroundServices/SetInactiveUsersService.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CleanArchitecture.Domain.Entities; +using CleanArchitecture.Domain.Enums; +using CleanArchitecture.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace CleanArchitecture.Api.BackgroundServices; + +public sealed class SetInactiveUsersService : BackgroundService +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + + public SetInactiveUsersService( + IServiceProvider serviceProvider, + ILogger logger) + { + _serviceProvider = serviceProvider; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + var scope = _serviceProvider.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + + IList inactiveUsers = Array.Empty(); + + try + { + inactiveUsers = await context.Users + .Where(user => + user.LastLoggedinDate < DateTime.UtcNow.AddDays(-30) && + user.Status == UserStatus.Active) + .Take(250) + .ToListAsync(stoppingToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while retrieving users to set inactive"); + } + + foreach (var user in inactiveUsers) + { + user.SetInactive(); + } + + try + { + await context.SaveChangesAsync(stoppingToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while setting users to inactive"); + } + + await Task.Delay(TimeSpan.FromDays(1), stoppingToken); + } + } +} \ No newline at end of file diff --git a/CleanArchitecture.Api/Program.cs b/CleanArchitecture.Api/Program.cs index b9819e7..332f47a 100644 --- a/CleanArchitecture.Api/Program.cs +++ b/CleanArchitecture.Api/Program.cs @@ -1,3 +1,4 @@ +using CleanArchitecture.Api.BackgroundServices; using CleanArchitecture.Api.Extensions; using CleanArchitecture.Application.Extensions; using CleanArchitecture.Application.gRPC; @@ -48,6 +49,8 @@ builder.Services.AddCommandHandlers(); builder.Services.AddNotificationHandlers(); builder.Services.AddApiUser(); +builder.Services.AddHostedService(); + builder.Services.AddMediatR(cfg => { cfg.RegisterServicesFromAssemblies(typeof(Program).Assembly); }); builder.Services.AddLogging(x => x.AddSimpleConsole(console => diff --git a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs index aee5fe6..081689c 100644 --- a/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/LoginUser/LoginUserCommandHandler.cs @@ -68,6 +68,14 @@ public sealed class LoginUserCommandHandler : CommandHandlerBase, return ""; } + + user.SetActive(); + user.SetLastLoggedinDate(DateTimeOffset.Now); + + if (!await CommitAsync()) + { + return ""; + } return BuildToken( user, diff --git a/CleanArchitecture.Domain/Entities/User.cs b/CleanArchitecture.Domain/Entities/User.cs index f33ea9d..292007c 100644 --- a/CleanArchitecture.Domain/Entities/User.cs +++ b/CleanArchitecture.Domain/Entities/User.cs @@ -10,6 +10,8 @@ public class User : Entity public string LastName { get; private set; } public string Password { get; private set; } public UserRole Role { get; private set; } + public UserStatus Status { get; private set; } + public DateTimeOffset? LastLoggedinDate { get; private set; } public string FullName => $"{FirstName}, {LastName}"; @@ -23,7 +25,8 @@ public class User : Entity string firstName, string lastName, string password, - UserRole role) : base(id) + UserRole role, + UserStatus status = UserStatus.Active) : base(id) { Email = email; TenantId = tenantId; @@ -31,6 +34,7 @@ public class User : Entity LastName = lastName; Password = password; Role = role; + Status = status; } public void SetEmail(string email) @@ -62,4 +66,19 @@ public class User : Entity { TenantId = tenantId; } + + public void SetLastLoggedinDate(DateTimeOffset lastLoggedinDate) + { + LastLoggedinDate = lastLoggedinDate; + } + + public void SetInactive() + { + Status = UserStatus.Inactive; + } + + public void SetActive() + { + Status = UserStatus.Active; + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Enums/UserStatus.cs b/CleanArchitecture.Domain/Enums/UserStatus.cs new file mode 100644 index 0000000..59866a2 --- /dev/null +++ b/CleanArchitecture.Domain/Enums/UserStatus.cs @@ -0,0 +1,7 @@ +namespace CleanArchitecture.Domain.Enums; + +public enum UserStatus +{ + Active, + Inactive +} \ No newline at end of file From 17bb1cbcf184b697136ea0a29a1dea5681ea2465 Mon Sep 17 00:00:00 2001 From: alex289 Date: Thu, 31 Aug 2023 20:49:27 +0200 Subject: [PATCH 2/3] feat: Add status to user viewmodel --- .../viewmodels/Users/UserViewModel.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CleanArchitecture.Application/viewmodels/Users/UserViewModel.cs b/CleanArchitecture.Application/viewmodels/Users/UserViewModel.cs index ea6e2da..01ed246 100644 --- a/CleanArchitecture.Application/viewmodels/Users/UserViewModel.cs +++ b/CleanArchitecture.Application/viewmodels/Users/UserViewModel.cs @@ -11,6 +11,7 @@ public sealed class UserViewModel public string FirstName { get; set; } = string.Empty; public string LastName { get; set; } = string.Empty; public UserRole Role { get; set; } + public UserStatus Status { get; set; } public static UserViewModel FromUser(User user) { @@ -20,7 +21,8 @@ public sealed class UserViewModel Email = user.Email, FirstName = user.FirstName, LastName = user.LastName, - Role = user.Role + Role = user.Role, + Status = user.Status }; } } \ No newline at end of file From 5ca2abadc662d4deb2c74b936e2c20c1936e2db1 Mon Sep 17 00:00:00 2001 From: Alexander Konietzko Date: Fri, 1 Sep 2023 08:48:23 +0200 Subject: [PATCH 3/3] chore: Add migration --- .../20230901064720_AddUserStatus.Designer.cs | 138 ++++++++++++++++++ .../20230901064720_AddUserStatus.cs | 47 ++++++ .../ApplicationDbContextModelSnapshot.cs | 7 + 3 files changed, 192 insertions(+) create mode 100644 CleanArchitecture.Infrastructure/Migrations/20230901064720_AddUserStatus.Designer.cs create mode 100644 CleanArchitecture.Infrastructure/Migrations/20230901064720_AddUserStatus.cs diff --git a/CleanArchitecture.Infrastructure/Migrations/20230901064720_AddUserStatus.Designer.cs b/CleanArchitecture.Infrastructure/Migrations/20230901064720_AddUserStatus.Designer.cs new file mode 100644 index 0000000..649cfcc --- /dev/null +++ b/CleanArchitecture.Infrastructure/Migrations/20230901064720_AddUserStatus.Designer.cs @@ -0,0 +1,138 @@ +// +using System; +using CleanArchitecture.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CleanArchitecture.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230901064720_AddUserStatus")] + partial class AddUserStatus + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.10") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CleanArchitecture.Domain.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.HasKey("Id"); + + b.ToTable("Tenants"); + + b.HasData( + new + { + Id = new Guid("b542bf25-134c-47a2-a0df-84ed14d03c4a"), + Deleted = false, + Name = "Admin Tenant" + }); + }); + + modelBuilder.Entity("CleanArchitecture.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("nvarchar(320)"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("LastLoggedinDate") + .HasColumnType("datetimeoffset"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("Users"); + + b.HasData( + new + { + Id = new Guid("7e3892c0-9374-49fa-a3fd-53db637a40ae"), + Deleted = false, + Email = "admin@email.com", + FirstName = "Admin", + LastName = "User", + Password = "$2a$12$Blal/uiFIJdYsCLTMUik/egLbfg3XhbnxBC6Sb5IKz2ZYhiU/MzL2", + Role = 0, + Status = 0, + TenantId = new Guid("b542bf25-134c-47a2-a0df-84ed14d03c4a") + }); + }); + + modelBuilder.Entity("CleanArchitecture.Domain.Entities.User", b => + { + b.HasOne("CleanArchitecture.Domain.Entities.Tenant", "Tenant") + .WithMany("Users") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("CleanArchitecture.Domain.Entities.Tenant", b => + { + b.Navigation("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CleanArchitecture.Infrastructure/Migrations/20230901064720_AddUserStatus.cs b/CleanArchitecture.Infrastructure/Migrations/20230901064720_AddUserStatus.cs new file mode 100644 index 0000000..868eeb0 --- /dev/null +++ b/CleanArchitecture.Infrastructure/Migrations/20230901064720_AddUserStatus.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CleanArchitecture.Infrastructure.Migrations +{ + /// + public partial class AddUserStatus : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LastLoggedinDate", + table: "Users", + type: "datetimeoffset", + nullable: true); + + migrationBuilder.AddColumn( + name: "Status", + table: "Users", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.UpdateData( + table: "Users", + keyColumn: "Id", + keyValue: new Guid("7e3892c0-9374-49fa-a3fd-53db637a40ae"), + columns: new[] { "LastLoggedinDate", "Status" }, + values: new object[] { null, 0 }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LastLoggedinDate", + table: "Users"); + + migrationBuilder.DropColumn( + name: "Status", + table: "Users"); + } + } +} diff --git a/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index 2c15819..831c467 100644 --- a/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -71,6 +71,9 @@ namespace CleanArchitecture.Infrastructure.Migrations .HasMaxLength(100) .HasColumnType("nvarchar(100)"); + b.Property("LastLoggedinDate") + .HasColumnType("datetimeoffset"); + b.Property("LastName") .IsRequired() .HasMaxLength(100) @@ -84,6 +87,9 @@ namespace CleanArchitecture.Infrastructure.Migrations b.Property("Role") .HasColumnType("int"); + b.Property("Status") + .HasColumnType("int"); + b.Property("TenantId") .HasColumnType("uniqueidentifier"); @@ -103,6 +109,7 @@ namespace CleanArchitecture.Infrastructure.Migrations LastName = "User", Password = "$2a$12$Blal/uiFIJdYsCLTMUik/egLbfg3XhbnxBC6Sb5IKz2ZYhiU/MzL2", Role = 0, + Status = 0, TenantId = new Guid("b542bf25-134c-47a2-a0df-84ed14d03c4a") }); });