Commit MWS backend source tree

The ASP.NET Core solution (mws.api/mws.application/mws.domain/
mws.infrastructure) existed only as untracked working files. Adding
it to version control, plus a .gitignore for build output and local
tooling directories, so the CQRS/mediator refactor plan has a real
git history to branch and diff against.
This commit is contained in:
2026-08-13 23:10:22 +07:00
parent 96944ad2c3
commit f72aaa2329
94 changed files with 7758 additions and 0 deletions
@@ -0,0 +1,41 @@
using Microsoft.EntityFrameworkCore;
using Mws.Application.Common.Repositories;
using Mws.Domain.Documents;
namespace Mws.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,88 @@
using Microsoft.EntityFrameworkCore;
using Mws.Application.Common.Repositories;
using Mws.Domain.Projects;
namespace Mws.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.FirstOrDefaultAsync(p => p.Id == projectId && p.Members.Any(m => m.UserId == userId), ct);
public Task<List<Project>> GetForUserAsync(Guid userId, CancellationToken ct = default) =>
Set.Where(p => p.Members.Any(m => m.UserId == userId))
.OrderByDescending(p => p.UpdatedAt)
.ToListAsync(ct);
public Task<List<Project>> SearchForUserAsync(Guid userId, string? term, CancellationToken ct = default)
{
var query = Set.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<List<ProjectMember>> GetMembersWithUserAsync(Guid projectId, CancellationToken ct = default) =>
Db.ProjectMembers
.Where(m => m.ProjectId == projectId)
.Include(m => m.User)
.OrderByDescending(m => m.Role)
.ThenBy(m => m.User.DisplayName)
.ToListAsync(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.Application.Common.Repositories;
namespace Mws.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,28 @@
using Microsoft.EntityFrameworkCore;
using Mws.Application.Common.Repositories;
using Mws.Domain.Roles;
namespace Mws.Infrastructure.Persistence.Repositories;
public class RoleRepository(AppDbContext db) : RepositoryBase<Role>(db), IRoleRepository
{
public Task<List<Role>> GetAllWithPermissionsAsync(CancellationToken ct = default) =>
Set.Include(r => r.Permissions).OrderBy(r => r.Name).ToListAsync(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 void RemovePermissions(IEnumerable<RolePermission> permissions) => Db.RolePermissions.RemoveRange(permissions);
}
@@ -0,0 +1,60 @@
using Microsoft.EntityFrameworkCore;
using Mws.Application.Common.Repositories;
using Mws.Domain.Tasks;
using TaskPriority = Mws.Domain.Tasks.TaskPriority;
using TaskStatus = Mws.Domain.Tasks.TaskStatus;
namespace Mws.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<List<TaskItem>> GetForProjectAsync(
Guid projectId, TaskStatus? status, TaskPriority? priority, Guid? assigneeId, 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).ToListAsync(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,44 @@
using Microsoft.EntityFrameworkCore;
using Mws.Application.Common.Repositories;
using Mws.Domain.Users;
namespace Mws.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.Role).FirstOrDefaultAsync(u => u.Id == id, ct);
public Task<User?> GetByUsernameWithRoleAsync(string username, CancellationToken ct = default) =>
Set.Include(u => u.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) =>
Set.AnyAsync(u => u.RoleId == roleId, ct);
public Task<Guid> GetRoleIdAsync(Guid userId, CancellationToken ct = default) =>
Set.Where(u => u.Id == userId).Select(u => u.RoleId).SingleOrDefaultAsync(ct);
public Task<List<User>> SearchWithRoleAsync(string? term, int? take, CancellationToken ct = default)
{
var query = Set.Include(u => u.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);
}
}