Зависимости на контракты

This commit is contained in:
Халимов Рустам
2026-03-30 17:48:39 +03:00
parent 09e5cbaa76
commit cb93ff7240
41 changed files with 620 additions and 344 deletions
@@ -0,0 +1,22 @@
namespace Knot.Contracts.Admin.Abstractions;
/// <summary>
/// Базовый класс для агрегатов - сущностей с бизнес-логикой и доменными событиями.
/// </summary>
public abstract class AggregateRoot<TId> : Entity<TId> where TId : notnull
{
private readonly List<IDomainEvent> _domainEvents = new();
public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
protected AggregateRoot(TId id) : base(id) { }
protected void RaiseDomainEvent(IDomainEvent domainEvent)
{
_domainEvents.Add(domainEvent);
}
public void ClearDomainEvents()
{
_domainEvents.Clear();
}
}
@@ -0,0 +1,43 @@
namespace Knot.Contracts.Admin.Abstractions;
/// <summary>
/// Базовый класс для сущностей с уникальным идентификатором.
/// </summary>
public abstract class Entity<TId> where TId : notnull
{
public TId Id { get; protected set; }
protected Entity(TId id)
{
Id = id;
}
public override bool Equals(object? obj)
{
if (obj is not Entity<TId> other)
return false;
if (ReferenceEquals(this, other))
return true;
if (GetType() != other.GetType())
return false;
return Id.Equals(other.Id);
}
public override int GetHashCode() => Id.GetHashCode();
public static bool operator ==(Entity<TId>? a, Entity<TId>? b)
{
if (a is null && b is null)
return true;
if (a is null || b is null)
return false;
return a.Equals(b);
}
public static bool operator !=(Entity<TId>? a, Entity<TId>? b) => !(a == b);
}
@@ -0,0 +1,15 @@
using MediatR;
namespace Knot.Contracts.Admin.Abstractions;
public interface ICommand : IRequest<Result> { }
public interface ICommand<TResponse> : IRequest<Result<TResponse>> { }
public interface ICommandHandler<in TCommand> : IRequestHandler<TCommand, Result>
where TCommand : ICommand
{ }
public interface ICommandHandler<in TCommand, TResponse> : IRequestHandler<TCommand, Result<TResponse>>
where TCommand : ICommand<TResponse>
{ }
@@ -0,0 +1,8 @@
using MediatR;
namespace Knot.Contracts.Admin.Abstractions;
/// <summary>
/// Интерфейс для доменных событий.
/// </summary>
public interface IDomainEvent : INotification { }
@@ -0,0 +1,13 @@
using MediatR;
namespace Knot.Contracts.Admin.Abstractions;
public interface IQuery<TResponse> : IRequest<Result<TResponse>>
{
}
public interface IQueryHandler<TQuery, TResponse>
: IRequestHandler<TQuery, Result<TResponse>>
where TQuery : IQuery<TResponse>
{
}
@@ -0,0 +1,3 @@
namespace Knot.Contracts.Admin.Abstractions;
public record MessageResponse(string Message);
@@ -0,0 +1,63 @@
namespace Knot.Contracts.Admin.Abstractions;
/// <summary>
/// Представляет ошибку в доменной логике.
/// </summary>
public sealed record Error(string Code, string Description)
{
public static readonly Error None = new(string.Empty, string.Empty);
public static Error NotFound(string code, string description) => new(code, description);
}
/// <summary>
/// Общая обертка для результата операции. Позволяет избегать использования исключений для управления потоком.
/// </summary>
public class Result
{
public bool IsSuccess { get; }
public bool IsFailure => !IsSuccess;
public Error Error { get; }
protected Result(bool isSuccess, Error error)
{
if (isSuccess && error != Error.None)
{
throw new InvalidOperationException();
}
if (!isSuccess && error == Error.None)
{
throw new InvalidOperationException();
}
IsSuccess = isSuccess;
Error = error;
}
public static Result Success() => new(true, Error.None);
public static Result Failure(Error error) => new(false, error);
public static Result<TValue> Success<TValue>(TValue value) => Result<TValue>.Success(value);
public static Result<TValue> Failure<TValue>(Error error) => Result<TValue>.Failure(error);
}
/// <summary>
/// Результ операции, содержащий значение.
/// </summary>
public class Result<TValue> : Result
{
private readonly TValue? _value;
protected internal Result(TValue? value, bool isSuccess, Error error)
: base(isSuccess, error)
{
_value = value;
}
public TValue Value => IsSuccess
? _value!
: throw new InvalidOperationException("Нельзя получить значение ошибочного результата.");
public static Result<TValue> Success(TValue value) => new(value, true, Error.None);
public new static Result<TValue> Failure(Error error) => new(default, false, error);
}
@@ -0,0 +1,3 @@
namespace Knot.Contracts.Admin.Abstractions;
public record SuccessResponse(bool Success);
@@ -6,10 +6,6 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Shared\Knot.Shared.Kernel\Knot.Shared.Kernel.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MediatR" Version="12.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.4" />
@@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Auth.Domain;
namespace Knot.Contracts.Auth.Infrastructure.Persistence;
public interface IAuthDbContext
{
IQueryable<UserContract> Users { get; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
@@ -1,20 +1,18 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Shared.Kernel;
using Knot.Contracts.Conversations.Domain;
namespace Knot.Contracts.Conversations.Infrastructure.Persistence;
public interface IChatsDbContext : IChatsUnitOfWork
public interface IChatsDbContext
{
IQueryable<Chat> Chats { get; }
}
public class Chat : AggregateRoot<Guid>
/// <summary>
/// Упрощенная проекция чата для запросов (не доменная сущность).
/// </summary>
public class Chat
{
public Chat() : base(Guid.NewGuid()) { }
public Guid Id { get; set; }
public string? Avatar { get; set; }
}
}
@@ -0,0 +1,9 @@
using System.Threading;
using System.Threading.Tasks;
namespace Knot.Contracts.Klipy.Application.Abstractions;
public interface IKlipyClient
{
Task<bool> TestConnectionAsync(string apiKey, string appName, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Knot.Contracts.Messaging.Application.Abstractions;
public interface IMessageQueryService
{
Task<List<MessageInfo>> GetAllMessagesAsync(CancellationToken cancellationToken);
Task<List<MessageInfo>> GetOrphanedMessagesAsync(HashSet<Guid> activeChatIds, CancellationToken cancellationToken);
Task DeleteMessagesAsync(List<Guid> messageIds, CancellationToken cancellationToken);
}
public record MessageInfo(Guid Id, Guid ChatId, bool IsDeleted, string? MediaUrl, List<MediaInfo>? Media);
public record MediaInfo(string Url);
@@ -0,0 +1,12 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Knot.Contracts.Messaging.Application.Abstractions;
public interface IUserStatsService
{
Task<Dictionary<Guid, UserStats>> GetStatsForUsersAsync(List<Guid> userIds, CancellationToken cancellationToken = default);
}
public record UserStats(int MessageCount, long StorageSize);
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Knot.Contracts.Settings.Abstractions;
public interface IStatisticsService
{
Task<DashboardStatsDto> GetDashboardStatsAsync(CancellationToken cancellationToken = default);
}
public class DashboardStatsDto
{
public long StorageUsedBytes { get; set; }
public long StorageLimitBytes { get; set; }
public long OnlineUsers { get; set; }
public long OfflineUsers { get; set; }
public long TotalUsers { get; set; }
public List<ActivityStatDto> ActivityTimeline { get; set; } = new();
public List<TopUserDto> TopUsersByMessages { get; set; } = new();
public List<TopUserDto> TopUsersByStorage { get; set; } = new();
}
public class ActivityStatDto
{
public DateTime Date { get; set; }
public long Messages { get; set; }
public long FilesSize { get; set; }
}
public class TopUserDto
{
public Guid UserId { get; set; }
public string Username { get; set; } = string.Empty;
public long Value { get; set; } // messages count or bytes
}
@@ -0,0 +1,25 @@
using System.IO;
using System.Threading.Tasks;
namespace Knot.Contracts.Storage.Abstractions;
public interface IFileStorageService
{
// Загружает поток файла и возвращает его уникальный идентификатор (SHA256 хеш или GUID).
Task<string> UploadFileAsync(Stream stream, string fileName, string contentType);
// Скачивает файл и возвращает его расшифрованный поток и тип содержимого.
Task<(Stream Stream, string ContentType, string FileName)> DownloadFileAsync(string fileId);
// Удаляет файл из хранилища.
Task DeleteFileAsync(string fileId);
// Получает список всех файлов в хранилище с их размерами.
Task<IEnumerable<(string FileId, long Size)>> ListFilesAsync();
// Получает размер конкретного файла
Task<long> GetFileSizeAsync(string fileId, CancellationToken ct = default);
// Получает размеры списка файлов
Task<Dictionary<string, long>> GetFileSizesAsync(IEnumerable<string> fileIds, CancellationToken ct = default);
}
@@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Knot.Contracts.Stories.Infrastructure.Persistence;
public interface IStoryCollection
{
Task<List<StoryContract>> GetAllAsync(CancellationToken cancellationToken);
}
public class StoryContract
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public string? MediaUrl { get; set; }
public string? MediaType { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? ExpiresAt { get; set; }
}