diff --git a/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs b/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs index 9f46f18..940cf53 100644 --- a/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs +++ b/CleanArchitecture.Application/Queries/Tenants/GetAll/GetAllTenantsQueryHandler.cs @@ -33,8 +33,8 @@ public async Task> Handle( var tenantsQuery = _tenantRepository .GetAllNoTracking() .IgnoreQueryFilters() - .Include(x => x.Users.Where(y => request.IncludeDeleted || !y.Deleted)) - .Where(x => request.IncludeDeleted || !x.Deleted); + .Include(x => x.Users.Where(y => request.IncludeDeleted || y.DeletedAt == null)) + .Where(x => request.IncludeDeleted || x.DeletedAt == null ); if (!string.IsNullOrWhiteSpace(request.SearchTerm)) { diff --git a/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQueryHandler.cs b/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQueryHandler.cs index 7de025a..71c85de 100644 --- a/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQueryHandler.cs +++ b/CleanArchitecture.Application/Queries/Users/GetAll/GetAllUsersQueryHandler.cs @@ -33,7 +33,7 @@ public async Task> Handle( var usersQuery = _userRepository .GetAllNoTracking() .IgnoreQueryFilters() - .Where(x => request.IncludeDeleted || !x.Deleted); + .Where(x => request.IncludeDeleted || x.DeletedAt == null); if (!string.IsNullOrWhiteSpace(request.SearchTerm)) { diff --git a/CleanArchitecture.Application/gRPC/TenantsApiImplementation.cs b/CleanArchitecture.Application/gRPC/TenantsApiImplementation.cs index 97679ac..c91a6b4 100644 --- a/CleanArchitecture.Application/gRPC/TenantsApiImplementation.cs +++ b/CleanArchitecture.Application/gRPC/TenantsApiImplementation.cs @@ -40,7 +40,7 @@ public override async Task GetByIds( { Id = tenant.Id.ToString(), Name = tenant.Name, - IsDeleted = tenant.Deleted + DeletedAt = tenant.DeletedAt == null ? "": tenant.DeletedAt.ToString() }) .ToListAsync(); diff --git a/CleanArchitecture.Application/gRPC/UsersApiImplementation.cs b/CleanArchitecture.Application/gRPC/UsersApiImplementation.cs index 6255920..6c24cf2 100644 --- a/CleanArchitecture.Application/gRPC/UsersApiImplementation.cs +++ b/CleanArchitecture.Application/gRPC/UsersApiImplementation.cs @@ -42,7 +42,7 @@ public override async Task GetByIds( Email = user.Email, FirstName = user.FirstName, LastName = user.LastName, - IsDeleted = user.Deleted + DeletedAt = user.DeletedAt == null ? "": user.DeletedAt.ToString() }) .ToListAsync(); diff --git a/CleanArchitecture.Domain/Entities/Entity.cs b/CleanArchitecture.Domain/Entities/Entity.cs index 41c7b54..df7d938 100644 --- a/CleanArchitecture.Domain/Entities/Entity.cs +++ b/CleanArchitecture.Domain/Entities/Entity.cs @@ -5,7 +5,7 @@ namespace CleanArchitecture.Domain.Entities; public abstract class Entity { public Guid Id { get; private set; } - public bool Deleted { get; private set; } + public DateTimeOffset? DeletedAt { get; private set; } protected Entity(Guid id) { @@ -24,11 +24,11 @@ public void SetId(Guid id) public void Delete() { - Deleted = true; + DeletedAt = DateTimeOffset.UtcNow; } public void Undelete() { - Deleted = false; + DeletedAt = null; } } \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure/Database/DbContextUtility.cs b/CleanArchitecture.Infrastructure/Database/DbContextUtility.cs index 29900c4..56f8492 100644 --- a/CleanArchitecture.Infrastructure/Database/DbContextUtility.cs +++ b/CleanArchitecture.Infrastructure/Database/DbContextUtility.cs @@ -9,17 +9,17 @@ public partial class ApplicationDbContext { public static class DbContextUtility { - public const string IsDeletedProperty = "Deleted"; + public const string IsDeletedProperty = "DeletedAt"; public static readonly MethodInfo PropertyMethod = typeof(EF) .GetMethod(nameof(EF.Property), BindingFlags.Static | BindingFlags.Public) - !.MakeGenericMethod(typeof(bool)); + !.MakeGenericMethod(typeof(DateTimeOffset?)); public static LambdaExpression GetIsDeletedRestriction(Type type) { var parm = Expression.Parameter(type, "it"); var prop = Expression.Call(PropertyMethod, parm, Expression.Constant(IsDeletedProperty)); - var condition = Expression.MakeBinary(ExpressionType.Equal, prop, Expression.Constant(false)); + var condition = Expression.MakeBinary(ExpressionType.Equal, prop, Expression.Constant(null)); var lambda = Expression.Lambda(condition, parm); return lambda; } diff --git a/CleanArchitecture.Infrastructure/Migrations/20241208214605_AddDeletedTimestamp.Designer.cs b/CleanArchitecture.Infrastructure/Migrations/20241208214605_AddDeletedTimestamp.Designer.cs new file mode 100644 index 0000000..0e544c0 --- /dev/null +++ b/CleanArchitecture.Infrastructure/Migrations/20241208214605_AddDeletedTimestamp.Designer.cs @@ -0,0 +1,136 @@ +// +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("20241208214605_AddDeletedTimestamp")] + partial class AddDeletedTimestamp + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .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("DeletedAt") + .HasColumnType("datetimeoffset"); + + 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"), + Name = "Admin Tenant" + }); + }); + + modelBuilder.Entity("CleanArchitecture.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); + + 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"), + 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.Restrict) + .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/20241208214605_AddDeletedTimestamp.cs b/CleanArchitecture.Infrastructure/Migrations/20241208214605_AddDeletedTimestamp.cs new file mode 100644 index 0000000..71ef3e6 --- /dev/null +++ b/CleanArchitecture.Infrastructure/Migrations/20241208214605_AddDeletedTimestamp.cs @@ -0,0 +1,95 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CleanArchitecture.Infrastructure.Migrations +{ + /// + public partial class AddDeletedTimestamp : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DeletedAt", + table: "Users", + type: "datetimeoffset", + nullable: true); + + migrationBuilder.AddColumn( + name: "DeletedAt", + table: "Tenants", + type: "datetimeoffset", + nullable: true); + + migrationBuilder.Sql("UPDATE Users SET DeletedAt = SYSDATETIMEOFFSET() WHERE Deleted = 1"); + migrationBuilder.Sql("UPDATE Tenants SET DeletedAt = SYSDATETIMEOFFSET() WHERE Deleted = 1"); + + migrationBuilder.DropColumn( + name: "Deleted", + table: "Users"); + + migrationBuilder.DropColumn( + name: "Deleted", + table: "Tenants"); + + migrationBuilder.UpdateData( + table: "Tenants", + keyColumn: "Id", + keyValue: new Guid("b542bf25-134c-47a2-a0df-84ed14d03c4a"), + column: "DeletedAt", + value: null); + + migrationBuilder.UpdateData( + table: "Users", + keyColumn: "Id", + keyValue: new Guid("7e3892c0-9374-49fa-a3fd-53db637a40ae"), + column: "DeletedAt", + value: null); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Deleted", + table: "Users", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "Deleted", + table: "Tenants", + type: "bit", + nullable: false, + defaultValue: false); + + migrationBuilder.Sql("UPDATE Users SET Deleted = true WHERE DeletedAt IS NOT NULL"); + migrationBuilder.Sql("UPDATE Tenants SET Deleted = true WHERE DeletedAt IS NOT NULL"); + + migrationBuilder.DropColumn( + name: "DeletedAt", + table: "Users"); + + migrationBuilder.DropColumn( + name: "DeletedAt", + table: "Tenants"); + + migrationBuilder.UpdateData( + table: "Tenants", + keyColumn: "Id", + keyValue: new Guid("b542bf25-134c-47a2-a0df-84ed14d03c4a"), + column: "Deleted", + value: false); + + migrationBuilder.UpdateData( + table: "Users", + keyColumn: "Id", + keyValue: new Guid("7e3892c0-9374-49fa-a3fd-53db637a40ae"), + column: "Deleted", + value: false); + } + } +} diff --git a/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index cdbe611..1d17fb5 100644 --- a/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "7.0.11") + .HasAnnotation("ProductVersion", "9.0.0") .HasAnnotation("Proxies:ChangeTracking", false) .HasAnnotation("Proxies:CheckEquality", false) .HasAnnotation("Proxies:LazyLoading", true) @@ -31,8 +31,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); - b.Property("Deleted") - .HasColumnType("bit"); + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); b.Property("Name") .IsRequired() @@ -47,7 +47,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) new { Id = new Guid("b542bf25-134c-47a2-a0df-84ed14d03c4a"), - Deleted = false, Name = "Admin Tenant" }); }); @@ -58,8 +57,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); - b.Property("Deleted") - .HasColumnType("bit"); + b.Property("DeletedAt") + .HasColumnType("datetimeoffset"); b.Property("Email") .IsRequired() @@ -103,7 +102,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) new { Id = new Guid("7e3892c0-9374-49fa-a3fd-53db637a40ae"), - Deleted = false, Email = "admin@email.com", FirstName = "Admin", LastName = "User", diff --git a/CleanArchitecture.Proto/Tenants/Models.proto b/CleanArchitecture.Proto/Tenants/Models.proto index c98c121..db17dba 100644 --- a/CleanArchitecture.Proto/Tenants/Models.proto +++ b/CleanArchitecture.Proto/Tenants/Models.proto @@ -5,7 +5,7 @@ option csharp_namespace = "CleanArchitecture.Proto.Tenants"; message Tenant { string id = 1; string name = 2; - bool isDeleted = 3; + optional string deletedAt = 3; } message GetTenantsByIdsResult { diff --git a/CleanArchitecture.Proto/Users/Models.proto b/CleanArchitecture.Proto/Users/Models.proto index c372420..f8e2e98 100644 --- a/CleanArchitecture.Proto/Users/Models.proto +++ b/CleanArchitecture.Proto/Users/Models.proto @@ -7,7 +7,7 @@ message GrpcUser { string firstName = 3; string lastName = 4; string email = 5; - bool isDeleted = 6; + optional string deletedAt = 6; } message GetUsersByIdsResult { diff --git a/CleanArchitecture.Shared/Tenants/TenantViewModel.cs b/CleanArchitecture.Shared/Tenants/TenantViewModel.cs index 97cd79f..bc015d9 100644 --- a/CleanArchitecture.Shared/Tenants/TenantViewModel.cs +++ b/CleanArchitecture.Shared/Tenants/TenantViewModel.cs @@ -4,4 +4,5 @@ namespace CleanArchitecture.Shared.Tenants; public sealed record TenantViewModel( Guid Id, - string Name); \ No newline at end of file + string Name, + DateTimeOffset? DeletedAt); \ No newline at end of file diff --git a/CleanArchitecture.Shared/Users/UserViewModel.cs b/CleanArchitecture.Shared/Users/UserViewModel.cs index d08d316..b94c55b 100644 --- a/CleanArchitecture.Shared/Users/UserViewModel.cs +++ b/CleanArchitecture.Shared/Users/UserViewModel.cs @@ -7,4 +7,4 @@ public sealed record UserViewModel( string Email, string FirstName, string LastName, - bool IsDeleted); \ No newline at end of file + DateTimeOffset? DeletedAt); \ No newline at end of file diff --git a/CleanArchitecture.gRPC/Contexts/TenantsContext.cs b/CleanArchitecture.gRPC/Contexts/TenantsContext.cs index f6038e4..6b3e7d0 100644 --- a/CleanArchitecture.gRPC/Contexts/TenantsContext.cs +++ b/CleanArchitecture.gRPC/Contexts/TenantsContext.cs @@ -27,6 +27,7 @@ public async Task> GetTenantsByIds(IEnumerable new TenantViewModel( Guid.Parse(tenant.Id), - tenant.Name)); + tenant.Name, + string.IsNullOrWhiteSpace(tenant.DeletedAt) ? null : DateTimeOffset.Parse(tenant.DeletedAt))); } } \ No newline at end of file diff --git a/CleanArchitecture.gRPC/Contexts/UsersContext.cs b/CleanArchitecture.gRPC/Contexts/UsersContext.cs index a6b02d2..03b27c9 100644 --- a/CleanArchitecture.gRPC/Contexts/UsersContext.cs +++ b/CleanArchitecture.gRPC/Contexts/UsersContext.cs @@ -30,6 +30,6 @@ public async Task> GetUsersByIds(IEnumerable id user.Email, user.FirstName, user.LastName, - user.IsDeleted)); + string.IsNullOrWhiteSpace(user.DeletedAt) ? null : DateTimeOffset.Parse(user.DeletedAt))); } } \ No newline at end of file