ref: project structure

This commit is contained in:
2026-08-19 23:25:08 +07:00
parent 05a93cfc09
commit a1fe31cf70
155 changed files with 3339 additions and 1007 deletions
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore;
using mws.backend.dotnet.domain.Documents;
using mws.backend.dotnet.domain.MasterData;
using mws.backend.dotnet.domain.Projects;
using mws.backend.dotnet.domain.Roles;
using mws.backend.dotnet.domain.Tasks;
using mws.backend.dotnet.domain.Users;
namespace mws.backend.dotnet.infrastructure.Persistence;
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
public DbSet<User> Users => Set<User>();
public DbSet<UserRole> UserRoles => Set<UserRole>();
public DbSet<Project> Projects => Set<Project>();
public DbSet<ProjectMember> ProjectMembers => Set<ProjectMember>();
public DbSet<ProjectMemberPermission> ProjectMemberPermissions => Set<ProjectMemberPermission>();
public DbSet<Document> Documents => Set<Document>();
public DbSet<TaskItem> Tasks => Set<TaskItem>();
public DbSet<Role> Roles => Set<Role>();
public DbSet<RolePermission> RolePermissions => Set<RolePermission>();
public DbSet<MasterDataEntry> MasterData => Set<MasterDataEntry>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
}
@@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using mws.backend.dotnet.domain.Documents;
namespace mws.backend.dotnet.infrastructure.Persistence.Configuration;
public class DocumentConfiguration : IEntityTypeConfiguration<Document>
{
public void Configure(EntityTypeBuilder<Document> e)
{
e.ToTable("documents");
e.HasKey(d => d.Id);
e.Property(d => d.Title).HasMaxLength(300).IsRequired();
e.Property(d => d.Type).HasConversion<string>().HasMaxLength(20);
e.Property(d => d.Content).HasColumnType("text");
e.HasIndex(d => new { d.ProjectId, d.ParentId });
e.HasIndex(d => d.ParentId);
e.HasOne(d => d.Parent)
.WithMany(d => d.Children)
.HasForeignKey(d => d.ParentId)
.OnDelete(DeleteBehavior.Restrict);
}
}
@@ -0,0 +1,18 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using mws.backend.dotnet.domain.MasterData;
namespace mws.backend.dotnet.infrastructure.Persistence.Configuration;
public class MasterDataEntryConfiguration : IEntityTypeConfiguration<MasterDataEntry>
{
public void Configure(EntityTypeBuilder<MasterDataEntry> e)
{
e.ToTable("master_data");
e.HasKey(m => m.Id);
e.Property(m => m.Group).HasMaxLength(100).IsRequired();
e.Property(m => m.Label).HasMaxLength(200).IsRequired();
e.Property(m => m.Value).HasMaxLength(100).IsRequired();
e.HasIndex(m => new { m.Group, m.Value }).IsUnique();
}
}
@@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using mws.backend.dotnet.domain.Projects;
namespace mws.backend.dotnet.infrastructure.Persistence.Configuration;
public class ProjectConfiguration : IEntityTypeConfiguration<Project>
{
public void Configure(EntityTypeBuilder<Project> e)
{
e.ToTable("projects");
e.HasKey(p => p.Id);
e.Property(p => p.Name).HasMaxLength(200).IsRequired();
e.Property(p => p.Description).HasMaxLength(2000);
e.Property(p => p.Status).HasConversion<string>().HasMaxLength(20);
e.HasIndex(p => p.CreatedBy);
e.HasIndex(p => p.UpdatedBy);
e.HasOne(p => p.CreatedByUser)
.WithMany()
.HasForeignKey(p => p.CreatedBy)
.OnDelete(DeleteBehavior.Restrict);
e.HasOne(p => p.UpdatedByUser)
.WithMany()
.HasForeignKey(p => p.UpdatedBy)
.OnDelete(DeleteBehavior.Restrict);
}
}
@@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using mws.backend.dotnet.domain.Projects;
namespace mws.backend.dotnet.infrastructure.Persistence.Configuration;
public class ProjectMemberConfiguration : IEntityTypeConfiguration<ProjectMember>
{
public void Configure(EntityTypeBuilder<ProjectMember> e)
{
e.ToTable("project_members");
e.HasKey(m => new { m.ProjectId, m.UserId });
e.Property(m => m.Role).HasConversion<string>().HasMaxLength(20);
e.HasOne(m => m.Project)
.WithMany(p => p.Members)
.HasForeignKey(m => m.ProjectId)
.OnDelete(DeleteBehavior.Cascade);
e.HasOne(m => m.User)
.WithMany()
.HasForeignKey(m => m.UserId)
.OnDelete(DeleteBehavior.Cascade);
}
}
@@ -0,0 +1,20 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using mws.backend.dotnet.domain.Projects;
namespace mws.backend.dotnet.infrastructure.Persistence.Configuration;
public class ProjectMemberPermissionConfiguration : IEntityTypeConfiguration<ProjectMemberPermission>
{
public void Configure(EntityTypeBuilder<ProjectMemberPermission> e)
{
e.ToTable("project_member_permissions");
e.HasKey(p => new { p.ProjectId, p.UserId, p.Screen });
e.Property(p => p.Screen).HasMaxLength(50).IsRequired();
e.HasOne<ProjectMember>()
.WithMany()
.HasForeignKey(p => new { p.ProjectId, p.UserId })
.OnDelete(DeleteBehavior.Cascade);
}
}
@@ -0,0 +1,16 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using mws.backend.dotnet.domain.Roles;
namespace mws.backend.dotnet.infrastructure.Persistence.Configuration;
public class RoleConfiguration : IEntityTypeConfiguration<Role>
{
public void Configure(EntityTypeBuilder<Role> e)
{
e.ToTable("roles");
e.HasKey(r => r.Id);
e.Property(r => r.Name).HasMaxLength(100).IsRequired();
e.HasIndex(r => r.Name).IsUnique();
}
}
@@ -0,0 +1,20 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using mws.backend.dotnet.domain.Roles;
namespace mws.backend.dotnet.infrastructure.Persistence.Configuration;
public class RolePermissionConfiguration : IEntityTypeConfiguration<RolePermission>
{
public void Configure(EntityTypeBuilder<RolePermission> e)
{
e.ToTable("role_permissions");
e.HasKey(p => new { p.RoleId, p.Screen });
e.Property(p => p.Screen).HasMaxLength(50).IsRequired();
e.HasOne(p => p.Role)
.WithMany(r => r.Permissions)
.HasForeignKey(p => p.RoleId)
.OnDelete(DeleteBehavior.Cascade);
}
}
@@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using mws.backend.dotnet.domain.Tasks;
namespace mws.backend.dotnet.infrastructure.Persistence.Configuration;
public class TaskConfiguration : IEntityTypeConfiguration<TaskItem>
{
public void Configure(EntityTypeBuilder<TaskItem> e)
{
e.ToTable("tasks");
e.HasKey(t => t.Id);
e.Property(t => t.Title).HasMaxLength(300).IsRequired();
e.Property(t => t.Description).HasColumnType("text");
e.Property(t => t.Status).HasConversion<string>().HasMaxLength(20);
e.Property(t => t.Priority).HasConversion<string>().HasMaxLength(20);
e.HasIndex(t => t.ProjectId);
e.HasIndex(t => t.AssigneeId);
e.HasIndex(t => t.Status);
e.HasOne(t => t.Assignee)
.WithMany()
.HasForeignKey(t => t.AssigneeId)
.OnDelete(DeleteBehavior.SetNull);
}
}
@@ -0,0 +1,18 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using mws.backend.dotnet.domain.Users;
namespace mws.backend.dotnet.infrastructure.Persistence.Configuration;
public class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> e)
{
e.ToTable("users");
e.HasKey(u => u.Id);
e.Property(u => u.Username).HasMaxLength(100).IsRequired();
e.HasIndex(u => u.Username).IsUnique();
e.Property(u => u.PasswordHash).HasMaxLength(200).IsRequired();
e.Property(u => u.DisplayName).HasMaxLength(150).IsRequired();
}
}
@@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using mws.backend.dotnet.domain.Users;
namespace mws.backend.dotnet.infrastructure.Persistence.Configuration;
public class UserRoleConfiguration : IEntityTypeConfiguration<UserRole>
{
public void Configure(EntityTypeBuilder<UserRole> e)
{
e.ToTable("user_roles");
e.HasKey(ur => new { ur.UserId, ur.RoleId });
e.HasOne(ur => ur.User)
.WithMany(u => u.UserRoles)
.HasForeignKey(ur => ur.UserId)
.OnDelete(DeleteBehavior.Cascade);
e.HasOne(ur => ur.Role)
.WithMany(r => r.UserRoles)
.HasForeignKey(ur => ur.RoleId)
.OnDelete(DeleteBehavior.Cascade);
}
}
+223
View File
@@ -0,0 +1,223 @@
using Microsoft.EntityFrameworkCore;
using mws.backend.dotnet.application.Auth;
using mws.backend.dotnet.application.Permissions;
using mws.backend.dotnet.domain.Documents;
using mws.backend.dotnet.domain.MasterData;
using mws.backend.dotnet.domain.Projects;
using mws.backend.dotnet.domain.Roles;
using mws.backend.dotnet.domain.Tasks;
using mws.backend.dotnet.domain.Users;
using TaskPriority = mws.backend.dotnet.domain.Tasks.TaskPriority;
using TaskStatus = mws.backend.dotnet.domain.Tasks.TaskStatus;
namespace mws.backend.dotnet.infrastructure.Persistence;
public static class DbSeeder
{
public static async Task SeedAsync(AppDbContext db, IPasswordHasher passwordHasher)
{
await SeedMasterDataAsync(db);
await EnsureRolesAsync(db);
var admin = await EnsureUserAsync(db, passwordHasher, "admin", "Admin", "Admin");
var alice = await EnsureUserAsync(db, passwordHasher, "alice", "Alice", "Member");
var bob = await EnsureUserAsync(db, passwordHasher, "bob", "Bob", "Member");
await EnsureUserAsync(db, passwordHasher, "namdh", "Nam DH", "Member");
if (await db.Projects.AnyAsync())
{
return;
}
var now = DateTime.UtcNow;
var project = new Project
{
Id = Guid.NewGuid(),
Name = "MWS",
Description = "My Workspace - internal tool for the team.",
Status = ProjectStatus.Active,
CreatedBy = admin.Id,
CreatedAt = now,
UpdatedBy = admin.Id,
UpdatedAt = now,
};
db.Projects.Add(project);
db.ProjectMembers.AddRange(
new ProjectMember { ProjectId = project.Id, UserId = admin.Id, Role = MemberRole.Owner },
new ProjectMember { ProjectId = project.Id, UserId = alice.Id, Role = MemberRole.Member },
new ProjectMember { ProjectId = project.Id, UserId = bob.Id, Role = MemberRole.Member });
db.ProjectMemberPermissions.AddRange(
new ProjectMemberPermission { ProjectId = project.Id, UserId = alice.Id, Screen = ProjectPermissionScreens.Documents, CanView = true },
new ProjectMemberPermission { ProjectId = project.Id, UserId = bob.Id, Screen = ProjectPermissionScreens.Documents, CanView = true });
await db.SaveChangesAsync();
var requirements = new Document
{
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = null,
Title = "Requirements", Type = DocumentType.Folder,
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
};
var design = new Document
{
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = null,
Title = "Technical Design", Type = DocumentType.Folder,
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
};
var notes = new Document
{
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = null,
Title = "Notes", Type = DocumentType.Folder,
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
};
db.Documents.AddRange(requirements, design, notes);
db.Documents.Add(new Document
{
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = requirements.Id,
Title = "Authentication", Type = DocumentType.Document,
Content = "<h2>Authentication</h2><p>Phase 1 uses simple username/password with JWT.</p>",
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
});
db.Documents.Add(new Document
{
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = requirements.Id,
Title = "User Management", Type = DocumentType.Document,
Content = "<h2>User Management</h2><p>Users are created via seeding in Phase 1.</p>",
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
});
db.Documents.Add(new Document
{
Id = Guid.NewGuid(), ProjectId = project.Id, ParentId = design.Id,
Title = "API Design", Type = DocumentType.Document,
Content = "<h2>API Design</h2><p>REST API with ASP.NET Core Web API.</p>",
CreatedBy = admin.Id, CreatedAt = now, UpdatedBy = admin.Id, UpdatedAt = now,
});
await db.SaveChangesAsync();
db.Tasks.AddRange(
new TaskItem
{
Id = Guid.NewGuid(), ProjectId = project.Id, Title = "Set up solution",
Status = TaskStatus.Done, Priority = TaskPriority.High, AssigneeId = admin.Id,
CreatedBy = admin.Id, CreatedAt = now, UpdatedAt = now,
},
new TaskItem
{
Id = Guid.NewGuid(), ProjectId = project.Id, Title = "Implement authentication",
Description = "Login endpoint with BCrypt and JWT.",
Status = TaskStatus.InProgress, Priority = TaskPriority.High, AssigneeId = alice.Id,
DueDate = now.AddDays(3),
CreatedBy = admin.Id, CreatedAt = now, UpdatedAt = now,
},
new TaskItem
{
Id = Guid.NewGuid(), ProjectId = project.Id, Title = "Task board drag & drop",
Status = TaskStatus.Todo, Priority = TaskPriority.Medium, AssigneeId = bob.Id,
DueDate = now.AddDays(7),
CreatedBy = admin.Id, CreatedAt = now, UpdatedAt = now,
},
new TaskItem
{
Id = Guid.NewGuid(), ProjectId = project.Id, Title = "Polish documents UI",
Status = TaskStatus.Todo, Priority = TaskPriority.Low,
CreatedBy = admin.Id, CreatedAt = now, UpdatedAt = now,
});
await db.SaveChangesAsync();
}
private static async Task EnsureRolesAsync(AppDbContext db)
{
var now = DateTime.UtcNow;
if (!await db.Roles.AnyAsync(r => r.Name == "Admin"))
{
var adminRole = new Role { Id = Guid.NewGuid(), Name = "Admin", IsSystem = true, CreatedAt = now, UpdatedAt = now };
adminRole.Permissions = ScreenCatalog.Screens.Select(s => new RolePermission
{
RoleId = adminRole.Id, Screen = s.Key, CanView = true, CanCreate = true, CanEdit = true, CanDelete = true,
}).ToList();
db.Roles.Add(adminRole);
await db.SaveChangesAsync();
}
if (!await db.Roles.AnyAsync(r => r.Name == "Member"))
{
var memberRole = new Role { Id = Guid.NewGuid(), Name = "Member", IsSystem = true, CreatedAt = now, UpdatedAt = now };
memberRole.Permissions = ScreenCatalog.Screens
.Where(s => s.Key is not ("users" or "permissions" or "masterdata"))
.Select(s => new RolePermission
{
RoleId = memberRole.Id, Screen = s.Key, CanView = true, CanCreate = true, CanEdit = true, CanDelete = false,
}).ToList();
db.Roles.Add(memberRole);
await db.SaveChangesAsync();
}
}
private static async Task<User> EnsureUserAsync(AppDbContext db, IPasswordHasher passwordHasher, string username, string displayName, string roleName)
{
var existing = await db.Users.Include(u => u.UserRoles).FirstOrDefaultAsync(u => u.Username == username);
if (existing != null)
{
return existing;
}
var role = await db.Roles.FirstAsync(r => r.Name == roleName);
var now = DateTime.UtcNow;
var user = new User
{
Id = Guid.NewGuid(),
Username = username,
PasswordHash = passwordHasher.Hash("password"),
DisplayName = displayName,
UserRoles = new List<UserRole> { new UserRole { RoleId = role.Id } },
CreatedAt = now,
UpdatedAt = now,
};
db.Users.Add(user);
await db.SaveChangesAsync();
return user;
}
private static async Task SeedMasterDataAsync(AppDbContext db)
{
if (await db.MasterData.AnyAsync())
{
return;
}
var now = DateTime.UtcNow;
MasterDataEntry Entry(string group, string label, string value, int sortOrder) => new()
{
Id = Guid.NewGuid(), Group = group, Label = label, Value = value, SortOrder = sortOrder,
IsActive = true, CreatedAt = now, UpdatedAt = now,
};
db.MasterData.AddRange(
Entry("task_status", "Todo", nameof(TaskStatus.Todo), 1),
Entry("task_status", "In Progress", nameof(TaskStatus.InProgress), 2),
Entry("task_status", "Done", nameof(TaskStatus.Done), 3),
Entry("task_status", "Cancelled", nameof(TaskStatus.Cancelled), 4),
Entry("task_priority", "Low", nameof(TaskPriority.Low), 1),
Entry("task_priority", "Medium", nameof(TaskPriority.Medium), 2),
Entry("task_priority", "High", nameof(TaskPriority.High), 3),
Entry("project_status", "Active", nameof(ProjectStatus.Active), 1),
Entry("project_status", "Archived", nameof(ProjectStatus.Archived), 2),
Entry("member_role", "Owner", nameof(MemberRole.Owner), 1),
Entry("member_role", "Member", nameof(MemberRole.Member), 2),
Entry("document_type", "Folder", nameof(DocumentType.Folder), 1),
Entry("document_type", "Document", nameof(DocumentType.Document), 2));
await db.SaveChangesAsync();
}
}
@@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore;
using mws.backend.dotnet.application.Common;
namespace mws.backend.dotnet.infrastructure.Persistence;
public static class PaginationExtensions
{
public const int DefaultPageSize = 20;
public const int MaxPageSize = 100;
public static async Task<PagedResult<T>> ToPagedResultAsync<T>(
this IQueryable<T> query, int page, int pageSize, CancellationToken ct = default)
{
page = page < 1 ? 1 : page;
pageSize = pageSize < 1 ? DefaultPageSize : Math.Min(pageSize, MaxPageSize);
var totalCount = await query.CountAsync(ct);
var items = await query.Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(ct);
return new PagedResult<T> { Items = items, TotalCount = totalCount, Page = page, PageSize = pageSize };
}
}
@@ -0,0 +1,41 @@
using Microsoft.EntityFrameworkCore;
using mws.backend.dotnet.application.Common.Repositories;
using mws.backend.dotnet.domain.Documents;
namespace mws.backend.dotnet.infrastructure.Persistence.Repositories;
public class DocumentRepository(AppDbContext db) : RepositoryBase<Document>(db), IDocumentRepository
{
public Task<Document?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
Set.FirstOrDefaultAsync(d => d.Id == id, ct);
public Task<List<Document>> GetTreeForProjectAsync(Guid projectId, CancellationToken ct = default) =>
Set.Where(d => d.ProjectId == projectId).OrderBy(d => d.Title).ToListAsync(ct);
public Task<Document?> GetInProjectAsync(Guid parentId, Guid projectId, CancellationToken ct = default) =>
Set.FirstOrDefaultAsync(d => d.Id == parentId && d.ProjectId == projectId, ct);
public Task<Guid?> GetParentIdAsync(Guid documentId, CancellationToken ct = default) =>
Set.Where(d => d.Id == documentId).Select(d => d.ParentId).SingleAsync(ct);
public Task<List<Document>> GetChildrenAsync(Guid parentId, CancellationToken ct = default) =>
Set.Where(d => d.ParentId == parentId).ToListAsync(ct);
public Task<List<Document>> SearchAsync(List<Guid> projectIds, string? term, int take, CancellationToken ct = default)
{
var query = Set.Where(d => projectIds.Contains(d.ProjectId));
if (!string.IsNullOrWhiteSpace(term))
{
var lower = term.Trim().ToLower();
query = query.Where(d => d.Title.ToLower().Contains(lower));
}
return query.OrderByDescending(d => d.UpdatedAt).Take(take).ToListAsync(ct);
}
public Task<int> CountByTypeForProjectAsync(Guid projectId, DocumentType type, CancellationToken ct = default) =>
Set.CountAsync(d => d.ProjectId == projectId && d.Type == type, ct);
public Task<List<Document>> GetRecentForProjectAsync(Guid projectId, DocumentType type, int take, CancellationToken ct = default) =>
Set.Where(d => d.ProjectId == projectId && d.Type == type).OrderByDescending(d => d.UpdatedAt).Take(take).ToListAsync(ct);
}
@@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore;
using mws.backend.dotnet.application.Common;
using mws.backend.dotnet.application.Common.Repositories;
using mws.backend.dotnet.domain.MasterData;
namespace mws.backend.dotnet.infrastructure.Persistence.Repositories;
public class MasterDataRepository(AppDbContext db) : RepositoryBase<MasterDataEntry>(db), IMasterDataRepository
{
public Task<PagedResult<MasterDataEntry>> GetAllAsync(string? group, int page, int pageSize, CancellationToken ct = default) =>
Set.Where(m => group == null || m.Group == group)
.OrderBy(m => m.Group).ThenBy(m => m.SortOrder).ThenBy(m => m.Label)
.ToPagedResultAsync(page, pageSize, ct);
public Task<List<MasterDataEntry>> GetActiveByGroupAsync(string group, CancellationToken ct = default) =>
Set.Where(m => m.Group == group && m.IsActive)
.OrderBy(m => m.SortOrder).ThenBy(m => m.Label)
.ToListAsync(ct);
public Task<MasterDataEntry?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
Set.FirstOrDefaultAsync(m => m.Id == id, ct);
public Task<bool> ExistsAsync(string group, string value, Guid? excludeId, CancellationToken ct = default) =>
Set.AnyAsync(m => m.Group == group && m.Value == value && (excludeId == null || m.Id != excludeId), ct);
}
@@ -0,0 +1,92 @@
using Microsoft.EntityFrameworkCore;
using mws.backend.dotnet.application.Common;
using mws.backend.dotnet.application.Common.Repositories;
using mws.backend.dotnet.domain.Projects;
namespace mws.backend.dotnet.infrastructure.Persistence.Repositories;
public class ProjectRepository(AppDbContext db) : RepositoryBase<Project>(db), IProjectRepository
{
public Task<Project?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
Set.FirstOrDefaultAsync(p => p.Id == id, ct);
public Task<Project?> GetForUserAsync(Guid userId, Guid projectId, CancellationToken ct = default) =>
Set.Include(p => p.CreatedByUser).Include(p => p.UpdatedByUser)
.FirstOrDefaultAsync(p => p.Id == projectId && p.Members.Any(m => m.UserId == userId), ct);
public Task<PagedResult<Project>> GetForUserAsync(Guid userId, int page, int pageSize, CancellationToken ct = default) =>
Set.Include(p => p.CreatedByUser).Include(p => p.UpdatedByUser)
.Where(p => p.Members.Any(m => m.UserId == userId))
.OrderByDescending(p => p.UpdatedAt)
.ToPagedResultAsync(page, pageSize, ct);
public Task<List<Project>> SearchForUserAsync(Guid userId, string? term, CancellationToken ct = default)
{
var query = Set.Include(p => p.CreatedByUser).Include(p => p.UpdatedByUser)
.Where(p => p.Members.Any(m => m.UserId == userId));
if (!string.IsNullOrWhiteSpace(term))
{
var lower = term.Trim().ToLower();
query = query.Where(p => p.Name.ToLower().Contains(lower));
}
return query.OrderByDescending(p => p.UpdatedAt).ToListAsync(ct);
}
public Task<int> CountMembersAsync(Guid projectId, CancellationToken ct = default) =>
Db.ProjectMembers.CountAsync(m => m.ProjectId == projectId, ct);
public Task<bool> IsMemberAsync(Guid projectId, Guid userId, CancellationToken ct = default) =>
Db.ProjectMembers.AnyAsync(m => m.ProjectId == projectId && m.UserId == userId, ct);
public Task<MemberRole?> GetMemberRoleAsync(Guid projectId, Guid userId, CancellationToken ct = default) =>
Db.ProjectMembers
.Where(m => m.ProjectId == projectId && m.UserId == userId)
.Select(m => (MemberRole?)m.Role)
.SingleOrDefaultAsync(ct);
public Task<ProjectMember?> GetMemberAsync(Guid projectId, Guid userId, CancellationToken ct = default) =>
Db.ProjectMembers.FirstOrDefaultAsync(m => m.ProjectId == projectId && m.UserId == userId, ct);
public Task<ProjectMember?> GetMemberWithUserAsync(Guid projectId, Guid userId, CancellationToken ct = default) =>
Db.ProjectMembers.Include(m => m.User).FirstOrDefaultAsync(m => m.ProjectId == projectId && m.UserId == userId, ct);
public Task<PagedResult<ProjectMember>> GetMembersWithUserAsync(Guid projectId, int page, int pageSize, CancellationToken ct = default) =>
Db.ProjectMembers
.Where(m => m.ProjectId == projectId)
.Include(m => m.User)
.OrderByDescending(m => m.Role)
.ThenBy(m => m.User.DisplayName)
.ToPagedResultAsync(page, pageSize, ct);
public Task<int> CountOwnersAsync(Guid projectId, CancellationToken ct = default) =>
Db.ProjectMembers.CountAsync(m => m.ProjectId == projectId && m.Role == MemberRole.Owner, ct);
public Task<List<Guid>> GetProjectIdsForUserAsync(Guid userId, CancellationToken ct = default) =>
Set.Where(p => p.Members.Any(m => m.UserId == userId)).Select(p => p.Id).ToListAsync(ct);
public Task<List<Guid>> GetOwnedProjectIdsAsync(Guid userId, CancellationToken ct = default) =>
Db.ProjectMembers.Where(m => m.UserId == userId && m.Role == MemberRole.Owner).Select(m => m.ProjectId).ToListAsync(ct);
public Task<List<Guid>> GetDocumentViewableProjectIdsAsync(Guid userId, CancellationToken ct = default) =>
Db.ProjectMembers
.Where(m => m.UserId == userId &&
(m.Role == MemberRole.Owner ||
Db.ProjectMemberPermissions.Any(p => p.ProjectId == m.ProjectId && p.UserId == userId && p.Screen == ProjectPermissionScreens.Documents && p.CanView)))
.Select(m => m.ProjectId)
.ToListAsync(ct);
public void AddMember(ProjectMember member) => Db.ProjectMembers.Add(member);
public void RemoveMember(ProjectMember member) => Db.ProjectMembers.Remove(member);
public Task<ProjectMemberPermission?> GetMemberPermissionAsync(Guid projectId, Guid userId, string screen, CancellationToken ct = default) =>
Db.ProjectMemberPermissions.FirstOrDefaultAsync(p => p.ProjectId == projectId && p.UserId == userId && p.Screen == screen, ct);
public Task<Dictionary<Guid, ProjectMemberPermission>> GetMemberPermissionsAsync(Guid projectId, string screen, CancellationToken ct = default) =>
Db.ProjectMemberPermissions
.Where(p => p.ProjectId == projectId && p.Screen == screen)
.ToDictionaryAsync(p => p.UserId, ct);
public void AddMemberPermission(ProjectMemberPermission permission) => Db.ProjectMemberPermissions.Add(permission);
}
@@ -0,0 +1,13 @@
using Microsoft.EntityFrameworkCore;
using mws.backend.dotnet.application.Common.Repositories;
namespace mws.backend.dotnet.infrastructure.Persistence.Repositories;
public abstract class RepositoryBase<T>(AppDbContext db) : IRepository<T> where T : class
{
protected readonly AppDbContext Db = db;
protected DbSet<T> Set => Db.Set<T>();
public void Add(T entity) => Set.Add(entity);
public void Remove(T entity) => Set.Remove(entity);
}
@@ -0,0 +1,32 @@
using Microsoft.EntityFrameworkCore;
using mws.backend.dotnet.application.Common;
using mws.backend.dotnet.application.Common.Repositories;
using mws.backend.dotnet.domain.Roles;
namespace mws.backend.dotnet.infrastructure.Persistence.Repositories;
public class RoleRepository(AppDbContext db) : RepositoryBase<Role>(db), IRoleRepository
{
public Task<PagedResult<Role>> GetAllWithPermissionsAsync(int page, int pageSize, CancellationToken ct = default) =>
Set.Include(r => r.Permissions).OrderBy(r => r.Name).ToPagedResultAsync(page, pageSize, ct);
public Task<Role?> GetByIdWithPermissionsAsync(Guid id, CancellationToken ct = default) =>
Set.Include(r => r.Permissions).FirstOrDefaultAsync(r => r.Id == id, ct);
public Task<Role?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
Set.FirstOrDefaultAsync(r => r.Id == id, ct);
public Task<bool> ExistsByNameAsync(string name, Guid? excludeId, CancellationToken ct = default) =>
Set.AnyAsync(r => r.Name == name && (excludeId == null || r.Id != excludeId), ct);
public Task<RolePermission?> GetPermissionAsync(Guid roleId, string screen, CancellationToken ct = default) =>
Db.RolePermissions.FirstOrDefaultAsync(p => p.RoleId == roleId && p.Screen == screen, ct);
public Task<Dictionary<string, RolePermission>> GetPermissionsAsync(Guid roleId, CancellationToken ct = default) =>
Db.RolePermissions.Where(p => p.RoleId == roleId).ToDictionaryAsync(p => p.Screen, ct);
public Task<List<RolePermission>> GetPermissionsForRolesAsync(IEnumerable<Guid> roleIds, CancellationToken ct = default) =>
Db.RolePermissions.Where(p => roleIds.Contains(p.RoleId)).ToListAsync(ct);
public void RemovePermissions(IEnumerable<RolePermission> permissions) => Db.RolePermissions.RemoveRange(permissions);
}
@@ -0,0 +1,61 @@
using Microsoft.EntityFrameworkCore;
using mws.backend.dotnet.application.Common;
using mws.backend.dotnet.application.Common.Repositories;
using mws.backend.dotnet.domain.Tasks;
using TaskPriority = mws.backend.dotnet.domain.Tasks.TaskPriority;
using TaskStatus = mws.backend.dotnet.domain.Tasks.TaskStatus;
namespace mws.backend.dotnet.infrastructure.Persistence.Repositories;
public class TaskRepository(AppDbContext db) : RepositoryBase<TaskItem>(db), ITaskRepository
{
public Task<TaskItem?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
Set.FirstOrDefaultAsync(t => t.Id == id, ct);
public Task<TaskItem?> GetWithAssigneeAsync(Guid id, CancellationToken ct = default) =>
Set.Include(t => t.Assignee).FirstOrDefaultAsync(t => t.Id == id, ct);
public Task<PagedResult<TaskItem>> GetForProjectAsync(
Guid projectId, TaskStatus? status, TaskPriority? priority, Guid? assigneeId, int page, int pageSize, CancellationToken ct = default)
{
var query = Set.Where(t => t.ProjectId == projectId);
if (status.HasValue)
{
query = query.Where(t => t.Status == status);
}
if (priority.HasValue)
{
query = query.Where(t => t.Priority == priority);
}
if (assigneeId.HasValue)
{
query = query.Where(t => t.AssigneeId == assigneeId);
}
return query.Include(t => t.Assignee).OrderByDescending(t => t.UpdatedAt).ToPagedResultAsync(page, pageSize, ct);
}
public Task<List<TaskItem>> SearchWithAssigneeAsync(List<Guid> projectIds, string? term, int take, CancellationToken ct = default)
{
var query = Set.Where(t => projectIds.Contains(t.ProjectId));
if (!string.IsNullOrWhiteSpace(term))
{
var lower = term.Trim().ToLower();
query = query.Where(t => t.Title.ToLower().Contains(lower));
}
return query.Include(t => t.Assignee).OrderByDescending(t => t.UpdatedAt).Take(take).ToListAsync(ct);
}
public async Task<Dictionary<TaskStatus, int>> CountByStatusForProjectAsync(Guid projectId, CancellationToken ct = default)
{
return await Set.Where(t => t.ProjectId == projectId)
.GroupBy(t => t.Status)
.Select(g => new { Status = g.Key, Count = g.Count() })
.ToDictionaryAsync(a => a.Status, a => a.Count, ct);
}
public Task<List<TaskItem>> GetRecentForProjectAsync(Guid projectId, int take, CancellationToken ct = default) =>
Set.Where(t => t.ProjectId == projectId).OrderByDescending(t => t.UpdatedAt).Take(take).ToListAsync(ct);
}
@@ -0,0 +1,57 @@
using Microsoft.EntityFrameworkCore;
using mws.backend.dotnet.application.Common;
using mws.backend.dotnet.application.Common.Repositories;
using mws.backend.dotnet.domain.Users;
namespace mws.backend.dotnet.infrastructure.Persistence.Repositories;
public class UserRepository(AppDbContext db) : RepositoryBase<User>(db), IUserRepository
{
public Task<User?> GetByIdAsync(Guid id, CancellationToken ct = default) =>
Set.FirstOrDefaultAsync(u => u.Id == id, ct);
public Task<User?> GetByIdWithRoleAsync(Guid id, CancellationToken ct = default) =>
Set.Include(u => u.UserRoles).ThenInclude(ur => ur.Role).FirstOrDefaultAsync(u => u.Id == id, ct);
public Task<User?> GetByUsernameWithRoleAsync(string username, CancellationToken ct = default) =>
Set.Include(u => u.UserRoles).ThenInclude(ur => ur.Role).SingleOrDefaultAsync(u => u.Username == username, ct);
public Task<bool> ExistsByUsernameAsync(string username, CancellationToken ct = default) =>
Set.AnyAsync(u => u.Username == username, ct);
public Task<bool> ExistsByRoleIdAsync(Guid roleId, CancellationToken ct = default) =>
Db.UserRoles.AnyAsync(ur => ur.RoleId == roleId, ct);
public Task<List<Guid>> GetRoleIdsAsync(Guid userId, CancellationToken ct = default) =>
Db.UserRoles.Where(ur => ur.UserId == userId).Select(ur => ur.RoleId).ToListAsync(ct);
public Task<List<User>> SearchWithRoleAsync(string? term, int? take, CancellationToken ct = default)
{
var query = Set.Include(u => u.UserRoles).ThenInclude(ur => ur.Role).AsQueryable();
if (!string.IsNullOrWhiteSpace(term))
{
var lower = term.Trim().ToLower();
query = query.Where(u => u.Username.ToLower().Contains(lower) || u.DisplayName.ToLower().Contains(lower));
}
query = query.OrderBy(u => u.DisplayName);
if (take is { } n)
{
query = query.Take(n);
}
return query.ToListAsync(ct);
}
public Task<PagedResult<User>> SearchWithRolePagedAsync(string? term, int page, int pageSize, CancellationToken ct = default)
{
var query = Set.Include(u => u.UserRoles).ThenInclude(ur => ur.Role).AsQueryable();
if (!string.IsNullOrWhiteSpace(term))
{
var lower = term.Trim().ToLower();
query = query.Where(u => u.Username.ToLower().Contains(lower) || u.DisplayName.ToLower().Contains(lower));
}
return query.OrderBy(u => u.DisplayName).ToPagedResultAsync(page, pageSize, ct);
}
}
+30
View File
@@ -0,0 +1,30 @@
using mws.backend.dotnet.application.Common;
using mws.backend.dotnet.application.Common.Repositories;
using mws.backend.dotnet.infrastructure.Persistence.Repositories;
namespace mws.backend.dotnet.infrastructure.Persistence;
public class UnitOfWork : IUnitOfWork
{
private readonly AppDbContext _db;
public UnitOfWork(AppDbContext db)
{
_db = db;
Users = new UserRepository(db);
Projects = new ProjectRepository(db);
Tasks = new TaskRepository(db);
Documents = new DocumentRepository(db);
Roles = new RoleRepository(db);
MasterData = new MasterDataRepository(db);
}
public IUserRepository Users { get; }
public IProjectRepository Projects { get; }
public ITaskRepository Tasks { get; }
public IDocumentRepository Documents { get; }
public IRoleRepository Roles { get; }
public IMasterDataRepository MasterData { get; }
public Task<int> SaveChangesAsync(CancellationToken ct = default) => _db.SaveChangesAsync(ct);
}