Switch to SmartRes Result/Error, upgrade to .NET 10

Migrate codebase to use SmartRes Result<T, Error> (and Unit) across services and interfaces, replacing previous Result usage and updating many method signatures and implementations. Add ResultExtensions to convert SmartRes results to ASP.NET ActionResult and refactor AuthController to use functional Bind/Tap chains for registration/login flows. Upgrade projects to .NET 10 and bump related NuGet packages; add libs/SmartRes.dll and project references. Simplify DbContext registration to use Npgsql only with retry policies and enable detailed logging. Update Swagger/launch settings and other minor fixes (AutoMapper registration change, whitespace/exception handling, and removal of the Govor.ConsoleClient files).
This commit is contained in:
Artemy
2026-07-25 20:20:45 +07:00
parent 6d1c53beeb
commit 3c785d5c89
65 changed files with 376 additions and 838 deletions
@@ -98,7 +98,7 @@ public static class ConfigurationProgramExtensions
// Auto Mapper
services.AddAutoMapper(typeof(MappingProfile));
services.AddAutoMapper(op => { }, typeof(MappingProfile));
services.AddScoped<IHubUserAccessor, HubUserAccessor>();
@@ -111,52 +111,26 @@ public static class ConfigurationProgramExtensions
services.AddScoped<IProfileService, ProfileService>();
}
public static void AddGovorDbContext(this IServiceCollection services, IConfiguration configuration)
{
var useMySql = configuration.GetValue<bool>("UseMySql");
if (useMySql)
services.AddDbContext<GovorDbContext>(options =>
{
services.AddDbContext<GovorDbContext>(options =>
{
var connectionString = configuration.GetConnectionString(nameof(GovorDbContext));
options.UseNpgsql(
configuration.GetConnectionString(nameof(GovorDbContext)),
npgsqlOptions =>
{
// retry for transient failures
npgsqlOptions.EnableRetryOnFailure(
5,
TimeSpan.FromSeconds(5),
null);
});
options.UseMySql(
connectionString,
new MySqlServerVersion(new Version(8, 0, 21)),
mySqlOptions =>
{
mySqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(5),
errorNumbersToAdd: null);
});
options.EnableSensitiveDataLogging();
options.EnableDetailedErrors();
});
}
else
{
services.AddDbContext<GovorDbContext>(options =>
{
options.UseNpgsql(
configuration.GetConnectionString(nameof(GovorDbContext)),
npgsqlOptions =>
{
// retry for transient failures
npgsqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(5),
errorCodesToAdd: null);
});
//options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
options.EnableSensitiveDataLogging();
options.EnableDetailedErrors();
});
}
//options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
options.EnableSensitiveDataLogging();
options.EnableDetailedErrors();
});
}
}
@@ -0,0 +1,60 @@
using Govor.Domain.Common;
using Microsoft.AspNetCore.Mvc;
using SmartRes;
namespace Govor.API.Common.Extensions;
public static class ResultExtensions
{
public static ActionResult ToActionResult<T>(this Result<T, Error> result)
{
if (result.IsSuccess)
{
// Если тип Unit, возвращаем 204 No Content, иначе 200 OK со значением
return typeof(T) == typeof(Unit)
? new StatusCodeResult(StatusCodes.Status204NoContent)
: new OkObjectResult(result.Value);
}
return GenerateProblemDetails(result.Error);
}
private static ActionResult GenerateProblemDetails(Error error)
{
var statusCode = error.Type switch
{
ErrorType.NotFound => StatusCodes.Status404NotFound,
ErrorType.Validation => StatusCodes.Status400BadRequest,
ErrorType.Conflict => StatusCodes.Status409Conflict,
ErrorType.Unauthorized => StatusCodes.Status401Unauthorized,
ErrorType.Forbidden => StatusCodes.Status403Forbidden,
_ => StatusCodes.Status400BadRequest
};
var problemDetails = new ProblemDetails
{
Status = statusCode,
Title = GetTitleForErrorType(error.Type),
Detail = error.Message,
};
problemDetails.Extensions.Add("errorCode", error.Code);
if (error.Errors is not null)
{
problemDetails.Extensions.Add("errors", error.Errors);
}
return new ObjectResult(problemDetails) { StatusCode = statusCode };
}
private static string GetTitleForErrorType(ErrorType type) => type switch
{
ErrorType.NotFound => "Not Found",
ErrorType.Validation => "Validation Error",
ErrorType.Conflict => "Conflict",
ErrorType.Unauthorized => "Unauthorized",
ErrorType.Forbidden => "Forbidden",
_ => "Bad Request"
};
}
@@ -1,9 +1,10 @@
using Govor.API.Common.Extensions;
using Govor.Application.Authentication;
using Govor.Application.Authentication.Exceptions;
using Govor.Application.Users.UserSessions;
using Govor.Contracts.Requests;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SmartRes;
namespace Govor.API.Controllers.Authentication;
@@ -30,82 +31,47 @@ public class AuthController : Controller
}
[HttpPost("register")] // api/auth/register
public async Task<IActionResult> Register([FromBody] RegistrationRequest registrationRequest)
public async Task<IActionResult> Register([FromBody] RegistrationRequest request)
{
var inviteResult = await _invitesService.ValidateAsync(registrationRequest.InviteLink);
if (!inviteResult.IsSuccess)
_logger.LogInformation("Processing registration request for: {Name}", request.Name);
var result = await _invitesService.ValidateAsync(request.InviteLink)
.BindAsync(invite => _accountService.RegistrationAsync(request.Name, request.Password, invite))
.TapAsync(user => _logger.LogInformation("User {Username} ({Id}) registered successfully", user.Username, user.Id))
.BindAsync(user => _userSession.OpenSessionAsync(user, request.DeviceInfo));
if (result.IsFailure)
{
_logger.LogWarning("Invite link invalid: {InviteLink}. Error: {Error}", registrationRequest.InviteLink,
inviteResult.Error);
return BadRequest($"Invite link invalid: {inviteResult.Error.Message}");
_logger.LogWarning("Registration pipeline failed. Error: {Code} - {Message}",
result.Error.Code, result.Error.Message);
}
else
{
_logger.LogInformation("Session opened successfully for the request.");
}
var userResult = await _accountService.RegistrationAsync(
registrationRequest.Name,
registrationRequest.Password,
inviteResult.Value);
if (userResult.IsFailure)
{
_logger.LogWarning("Registration failed for user {Name}. Error: {Error}", registrationRequest.Name,
userResult.Error);
return userResult.Error.Code switch
{
nameof(UserAlreadyExistException) => BadRequest($"Registration failed: {userResult.Error.Message}"),
nameof(InvalidUsernameException) => BadRequest($"Invalid username: {userResult.Error.Message}"),
_ => BadRequest($"Registration failed: {userResult.Error.Message}")
};
}
var user = userResult.Value;
_logger.LogInformation("Register request for {Username} with id {Id} processed successfully", user.Username,
user.Id);
var sessionResult = await _userSession.OpenSessionAsync(user, registrationRequest.DeviceInfo);
if (sessionResult.IsFailure)
{
_logger.LogError("Failed to open session for user {Username}. Error: {Error}", user.Username,
sessionResult.Error.Message);
return StatusCode(500, "An error occurred while creating the session.");
}
_logger.LogInformation("Session for user {Username} with id {Id} has been opened", user.Username, user.Id);
return Ok(sessionResult.Value);
return result.ToActionResult();
}
[HttpPost("login")] // api/auth/login
public async Task<IActionResult> Login([FromBody] LoginRequest loginRequest)
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
var userResult = await _accountService.LoginAsync(loginRequest.Name, loginRequest.Password);
_logger.LogInformation("Processing registration request for: {Name}", request.Name);
var result = await _accountService.LoginAsync(request.Name, request.Password)
.TapAsync(user => _logger.LogInformation("User {Username} ({Id}) logged in.", user.Username, user.Id))
.BindAsync(user => _userSession.OpenSessionAsync(user, request.DeviceInfo));
if (userResult.IsFailure)
if (result.IsFailure)
{
_logger.LogWarning("Login failed for user {Name}. Error: {Code}", loginRequest.Name, userResult.Error);
return userResult.Error.Code switch
{
nameof(UserNotRegisteredException) => BadRequest("Login failed: user does not exist."),
nameof(InvalidOperationException) => BadRequest("Login failed: username or password is incorrect."),
_ => BadRequest($"Login failed: {userResult.Error.Message}")
};
_logger.LogWarning("Login pipeline failed. Error: {Code} - {Message}",
result.Error.Code, result.Error.Message);
}
else
{
_logger.LogInformation("Session opened successfully for the request.");
}
var user = userResult.Value;
_logger.LogInformation("Login request for {Username} with id {Id} processed successfully", user.Username, user.Id);
var sessionResult = await _userSession.OpenSessionAsync(user, loginRequest.DeviceInfo);
if (sessionResult.IsFailure)
{
_logger.LogError("Failed to open session for user {Username}. Error: {Error}", user.Username, sessionResult.Error);
return StatusCode(500, "An error occurred while creating the session.");
}
_logger.LogInformation("Session for user {Username} with id {Id} has been opened", user.Username, user.Id);
return Ok(sessionResult.Value);
return result.ToActionResult();
}
}
@@ -44,7 +44,7 @@ public class FriendshipController : Controller
{
_logger.LogWarning(ex, ex.Message);
return Forbid(ex.Message);
}
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
+18 -12
View File
@@ -1,27 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="FirebaseAdmin" Version="3.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.5" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6" />
<PackageReference Include="BCrypt.Net-Next" Version="4.2.0" />
<PackageReference Include="FirebaseAdmin" Version="3.6.0" />
<PackageReference Include="AutoMapper" Version="16.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.10" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.3" />
<PackageReference Include="NSwag.AspNetCore" Version="14.4.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.1" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Microting.EntityFrameworkCore.MySql" Version="10.0.10" />
</ItemGroup>
<ItemGroup>
<Reference Include="SmartRes">
<HintPath>..\libs\SmartRes.dll</HintPath>
</Reference>
<ProjectReference Include="..\Govor.Application\Govor.Application.csproj" />
<ProjectReference Include="..\Govor.Contracts\Govor.Contracts.csproj" />
<ProjectReference Include="..\Govor.Domain\Govor.Domain.csproj" />
+20 -14
View File
@@ -7,7 +7,7 @@ using Govor.Application.Authentication.JWT;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi;
var builder = WebApplication.CreateBuilder(args);
@@ -86,28 +86,34 @@ builder.Services.AddGovorDbContext(configuration); // GovorDbContext init
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
services.AddSwaggerGen(options =>
{
const string schemeId = "Bearer";
options.SwaggerDoc("v1", new OpenApiInfo { Title = "Govor API", Version = "v1" });
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
options.AddSecurityDefinition(schemeId, new OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme. Example: 'Bearer {token}'",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.Http,
Scheme = "bearer"
In = ParameterLocation.Header,
Scheme = "bearer",
BearerFormat = "JWT",
Description = "JWT Authorization header using the Bearer scheme. Example: 'Bearer {token}'"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
options.AddSecurityRequirement(document =>
{
var requirement = new OpenApiSecurityRequirement
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" }
},
Array.Empty<string>()
}
new OpenApiSecuritySchemeReference(schemeId)
{
Reference = new OpenApiReferenceWithDescription { Type = ReferenceType.SecurityScheme, Id = "Bearer" }
},
[]
}
};
return requirement;
});
});
@@ -121,7 +127,7 @@ if (!app.Environment.IsDevelopment())
{
//app.MapOpenApi();
builder.WebHost.UseUrls("http://0.0.0.0:8080");
builder.WebHost.UseUrls("http://10.8.0.5:5000");
//builder.WebHost.UseUrls("http://10.8.0.5:5000");
//builder.WebHost.UseUrls("http://192.168.1.107:8080");
}
+1 -1
View File
@@ -5,7 +5,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://0.0.0.0:8080;http://localhost:7155;http://10.8.0.5:5000",
"applicationUrl": "http://0.0.0.0:8080;http://localhost:7155",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
@@ -6,6 +6,7 @@ using Govor.Domain.Common;
using Govor.Domain.Models;
using Govor.Domain.Models.Users;
using Microsoft.EntityFrameworkCore;
using SmartRes;
namespace Govor.Application.Authentication;
@@ -28,18 +29,18 @@ public class AuthService : IAccountService
_usernameValidator = usernameValidator;
}
public async Task<Result<User>> RegistrationAsync(string name, string password, Invitation invitation)
public async Task<Result<User, Error>> RegistrationAsync(string name, string password, Invitation invitation)
{
var validationResult = _usernameValidator.Validate(name);
if (validationResult.IsFailure)
{
return Result<User>.Failure(validationResult.Error);
return Result.Failure<User>(validationResult.Error);
}
if (await _userNameExistValidator.IsUsernameExistsAsync(name))
{
return Result<User>.Failure(new Error(
return Result.Failure<User>(Error.Conflict(
nameof(UserAlreadyExistException),
$"User with username '{name}' already exists."));
}
@@ -63,12 +64,14 @@ public class AuthService : IAccountService
await SetRoleAsync(user, invitation);
// TODO: inv.participantCount -= 1; db.save();
await _context.SaveChangesAsync();
return user; // Success
}
public async Task<Result<User>> LoginAsync(string name, string password)
public async Task<Result<User, Error>> LoginAsync(string name, string password)
{
var user = await _context.Users
.AsNoTracking()
@@ -76,14 +79,14 @@ public class AuthService : IAccountService
if (user is null)
{
return Result<User>.Failure(new Error(
return Result.Failure<User>(Error.NotFound(
nameof(UserNotRegisteredException),
$"User '{name}' is not registered."));
}
if (!_passwordHasher.Verify(password, user.PasswordHash))
{
return Result<User>.Failure(new Error(
return Result.Failure<User>(Error.Failure(
nameof(InvalidOperationException),
"The password provided is incorrect."));
}
@@ -1,11 +1,12 @@
using Govor.Domain.Common;
using Govor.Domain.Models;
using Govor.Domain.Models.Users;
using SmartRes;
namespace Govor.Application.Authentication;
public interface IAccountService
{
public Task<Result<User>> RegistrationAsync(string name, string password, Invitation invitation);
public Task<Result<User>> LoginAsync(string name, string password);
public Task<Result<User, Error>> RegistrationAsync(string name, string password, Invitation invitation);
public Task<Result<User, Error>> LoginAsync(string name, string password);
}
@@ -1,6 +1,7 @@
using Govor.Domain.Common;
using Govor.Domain.Models;
using Govor.Domain.Models.Users;
using SmartRes;
namespace Govor.Application.Authentication;
@@ -8,5 +9,5 @@ public interface IInvitesService
{
public Task<string> GetRoleNameAsync(User user);
public Task<string> GetRoleNameAsync(Guid sessionId);
public Task<Result<Invitation>> ValidateAsync(string inviteCode);
public Task<Result<Invitation, Error>> ValidateAsync(string inviteCode);
}
@@ -4,6 +4,7 @@ using Govor.Domain.Common;
using Govor.Domain.Models;
using Govor.Domain.Models.Users;
using Microsoft.EntityFrameworkCore;
using SmartRes;
namespace Govor.Application.Authentication;
@@ -31,22 +32,24 @@ public class InvitesService : IInvitesService
return invitation.IsAdmin ? "Admin" : "User";
}
public async Task<Result<Invitation>> ValidateAsync(string inviteCode)
public async Task<Result<Invitation, Error>> ValidateAsync(string inviteCode)
{
var invite = await _context.Invitations
.Include(s => s.Users)
.FirstOrDefaultAsync(s => s.Code == inviteCode);
if (invite == null)
return Result<Invitation>.Failure(Error.Null);
return Result.Failure<Invitation>(Error.NotFound("Auth.LinkNotFount","Invitation not found."));
if (invite.EndDate < DateTime.Now || invite.MaxParticipants <= invite.Users.Count)
{
invite.IsActive = false;
await _context.SaveChangesAsync();
return Result<Invitation>.Failure(new Error(
"Auth.InviteLinkInvalid", $"Invite link invalid: {inviteCode}")
return Result.Failure<Invitation>(
Error.Failure(
"Auth.InviteLinkInvalid", $"Invite link invalid: {inviteCode}"
)
);
}
@@ -5,6 +5,7 @@ using Govor.Domain;
using Govor.Domain.Common;
using Govor.Domain.Models;
using Microsoft.EntityFrameworkCore;
using SmartRes;
namespace Govor.Application.Friends;
@@ -21,10 +22,10 @@ public class FriendRequestCommandService : IFriendRequestCommandService
_privateChatsCreator = privateChatsCreator;
}
public async Task<Result<Friendship>> SendAsync(Guid fromUserId, Guid toUserId)
public async Task<Result<Friendship, Error>> SendAsync(Guid fromUserId, Guid toUserId)
{
if (fromUserId == toUserId)
return Result<Friendship>.Failure(new Error(
return Result.Failure<Friendship>(Error.Failure(
"Friendship.Send",
"Cannot send a request to self user")
);
@@ -50,7 +51,7 @@ public class FriendRequestCommandService : IFriendRequestCommandService
friendship.Status == FriendshipStatus.Accepted ||
friendship.Status == FriendshipStatus.Blocked)
{
return Result<Friendship>.Failure(new Error(
return Result.Failure<Friendship>(Error.Conflict(
"Friendship.Send",
$"The request is already {friendship.Status}")
);
@@ -65,24 +66,24 @@ public class FriendRequestCommandService : IFriendRequestCommandService
return friendship;
}
public async Task<Result<Friendship>> AcceptAsync(Guid requestId, Guid currentUserId)
public async Task<Result<Friendship, Error>> AcceptAsync(Guid requestId, Guid currentUserId)
{
var friendship = await _context.Friendships.FindAsync(requestId);
if (friendship is null)
return Result<Friendship>.Failure(new Error(
return Result.Failure<Friendship>(Error.NotFound(
"Friendship.Accept",
"Friendship not found! You cant accept request!")
);
if (friendship.AddresseeId != currentUserId)
return Result<Friendship>.Failure(new Error(
return Result.Failure<Friendship>(Error.Forbidden(
"Friendship.Accept",
"You cannot accept this request!")
);
if (friendship.Status != FriendshipStatus.Pending)
return Result<Friendship>.Failure(new Error(
return Result.Failure<Friendship>(Error.Forbidden(
"Friendship.Accept",
"Request is already accepted!")
);
@@ -96,24 +97,24 @@ public class FriendRequestCommandService : IFriendRequestCommandService
return friendship;
}
public async Task<Result<Friendship>> RejectAsync(Guid requestId, Guid currentUserId)
public async Task<Result<Friendship, Error>> RejectAsync(Guid requestId, Guid currentUserId)
{
var friendship = await _context.Friendships.FindAsync(requestId);
if (friendship == null)
return Result<Friendship>.Failure(new Error(
return Result.Failure<Friendship>(Error.NotFound(
"Friendship.Reject",
"Friendship not found! You cant reject request!")
);
if (friendship.AddresseeId != currentUserId)
return Result<Friendship>.Failure(new Error(
return Result.Failure<Friendship>(Error.Forbidden(
"Friendship.Reject",
"You cannot reject this request!")
);
if (friendship.Status != FriendshipStatus.Pending && friendship.Status != FriendshipStatus.Rejected)
return Result<Friendship>.Failure(new Error(
return Result.Failure<Friendship>(Error.Conflict(
"Friendship.Reject",
$"Request is already {friendship.Status}")
);
@@ -1,11 +1,12 @@
using Govor.Domain.Common;
using Govor.Domain.Models;
using SmartRes;
namespace Govor.Application.Friends;
public interface IFriendRequestCommandService
{
Task<Result<Friendship>> SendAsync(Guid fromUserId, Guid toUserId);
Task<Result<Friendship>> AcceptAsync(Guid requestId, Guid currentUserId);
Task<Result<Friendship>> RejectAsync(Guid requestId, Guid currentUserId);
Task<Result<Friendship, Error>> SendAsync(Guid fromUserId, Guid toUserId);
Task<Result<Friendship, Error>> AcceptAsync(Guid requestId, Guid currentUserId);
Task<Result<Friendship, Error>> RejectAsync(Guid requestId, Guid currentUserId);
}
+18 -10
View File
@@ -1,23 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="FirebaseAdmin" Version="3.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="11.0.0-preview.1.26104.118" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.0.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" />
<PackageReference Include="BCrypt.Net-Next" Version="4.2.0" />
<PackageReference Include="FirebaseAdmin" Version="3.6.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.10" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version = "8.19.2"/>
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.19.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.19.2" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<Reference Include="SmartRes">
<HintPath>..\libs\SmartRes.dll</HintPath>
</Reference>
<ProjectReference Include="..\Govor.Domain\Govor.Domain.csproj" />
</ItemGroup>
+4 -3
View File
@@ -1,6 +1,7 @@
using Govor.Domain.Common;
using Govor.Domain.Models;
using Govor.Domain.Models.Users;
using SmartRes;
namespace Govor.Application.Groups;
@@ -8,9 +9,9 @@ public interface IGroupService
{
Task<ChatGroup> GetGroupByIdAsync(Guid groupId);
Task<ChatGroup> CreateGroupAsync(string name, Guid creatorId, IEnumerable<Guid> initialMemberIds);
Task<Result> AddUserToGroupByInvitationAsync(Guid userId, string invitationCode);
Task<Result> RemoveUserFromGroupAsync(Guid groupId, Guid userId, Guid removedByUserId);
Task<Result> DeleteGroupAsync(Guid groupId, Guid userId);
Task<Result<Unit, Error>> AddUserToGroupByInvitationAsync(Guid userId, string invitationCode);
Task<Result<Unit, Error>> RemoveUserFromGroupAsync(Guid groupId, Guid userId, Guid removedByUserId);
Task<Result<Unit, Error>> DeleteGroupAsync(Guid groupId, Guid userId);
Task<List<User>> GetGroupMembersAsync(Guid groupId);
Task<List<ChatGroup>>GetUserGroupsAsync(Guid userId);
ChatGroup GetGroupByInviteCode(string code);
@@ -1,10 +1,11 @@
using Govor.Domain.Common;
using Govor.Domain.Models;
using SmartRes;
namespace Govor.Application.Infrastructure.AdminsStuff;
public interface IInvitationGetter
{
Task<List<Invitation>> GetAllAsync();
Task<Result<Invitation>> FindByIdAsync(Guid id);
Task<Result<Invitation, Error>> FindByIdAsync(Guid id);
}
@@ -3,6 +3,7 @@ using Govor.Domain.Common;
using Govor.Domain.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SmartRes;
namespace Govor.Application.Infrastructure.AdminsStuff;
@@ -25,13 +26,13 @@ public class InvitationGetter : IInvitationGetter
.ToListAsync();
}
public async Task<Result<Invitation>> FindByIdAsync(Guid id)
public async Task<Result<Invitation, Error>> FindByIdAsync(Guid id)
{
var res = await _context.Invitations.AsNoTracking()
.FirstOrDefaultAsync(iv => iv.Id == id);
if (res is null)
return Result<Invitation>.Failure(new Error(
return Result.Failure<Invitation>(Error.NotFound(
nameof(InvalidOperationException),
"Invitation not found.")
);
@@ -1,9 +1,10 @@
using Govor.Domain.Common;
using SmartRes;
namespace Govor.Application.Infrastructure.Validators;
public interface IUsernameValidator
{
Result Validate(string username);
Result<Unit, Error> Validate(string username);
bool TryValidate(string username);
}
@@ -3,6 +3,7 @@ using Govor.Application.Authentication.Exceptions;
using Govor.Domain.Common.Constants;
using Govor.Domain.Common;
using Microsoft.Extensions.Configuration;
using SmartRes;
namespace Govor.Application.Infrastructure.Validators;
@@ -39,52 +40,58 @@ public class UsernameValidator : IUsernameValidator
?? throw new InvalidOperationException("Reserved not set");
}
public Result Validate(string username)
public Result<Unit, Error> Validate(string username)
{
if (username.Length < UserConstants.MIN_LENGHT_OF_NAME || username.Length > UserConstants.MAX_LENGHT_OF_NAME)
{
return new Error(
return Result.Failure(Error.Validation(
ErrorCode,
$"Username must be between {UserConstants.MIN_LENGHT_OF_NAME} and {UserConstants.MAX_LENGHT_OF_NAME} characters.");
$"Username must be between {UserConstants.MIN_LENGHT_OF_NAME} and {UserConstants.MAX_LENGHT_OF_NAME} characters.")
);
}
if (!_usernameRegex.IsMatch(username))
{
return new Error(
return Result.Failure(Error.Validation(
ErrorCode,
"The username must be in Cyrillic and start with a letter.");
"The username must be in Cyrillic and start with a letter.")
);
}
if (Regex.IsMatch(username, @"(.)\1{4,}"))
{
return new Error(
return Result.Failure(Error.Validation(
ErrorCode,
"Too many repeating characters.");
"Too many repeating characters.")
);
}
var normalized = Normalize(username);
if (_reserved.Contains(normalized))
{
return new Error(
return Result.Failure(Error.Validation(
ErrorCode,
"This username is reserved.");
"This username is reserved.")
);
}
if (_blockedExact.Contains(normalized))
{
return new Error(
return Result.Failure(Error.Validation(
ErrorCode,
"This username is not allowed.");
"This username is not allowed.")
);
}
foreach (var banned in _blockedContains)
{
if (normalized.Contains(banned))
{
return new Error(
return Result.Failure(Error.Validation(
ErrorCode,
"Username contains prohibited content.");
"Username contains prohibited content.")
);
}
}
@@ -1,11 +1,12 @@
using Govor.Domain.Common;
using Govor.Domain.Models;
using SmartRes;
namespace Govor.Application.PrivateUserChats;
public interface IUserPrivateChatsGetterService
{
Task<List<PrivateChat>> GetUserChatsAsync(Guid userId);
Task<Result<PrivateChat>> GetPrivateChatAsync(Guid chatId);
Task<Result<PrivateChat, Error>> GetPrivateChatAsync(Guid chatId);
Task<bool> ExistChatAsync(Guid userIdA, Guid userIdB);
}
@@ -2,6 +2,7 @@ using Govor.Domain;
using Govor.Domain.Common;
using Govor.Domain.Models;
using Microsoft.EntityFrameworkCore;
using SmartRes;
namespace Govor.Application.PrivateUserChats;
@@ -22,14 +23,16 @@ public class UserPrivateChatsGetter : IUserPrivateChatsGetterService
.ToListAsync();
}
public async Task<Result<PrivateChat>> GetPrivateChatAsync(Guid chatId)
public async Task<Result<PrivateChat, Error>> GetPrivateChatAsync(Guid chatId)
{
var res = await _context.PrivateChats.AsNoTracking()
.FirstOrDefaultAsync(p => p.Id == chatId);
if (res == null)
return Result<PrivateChat>.Failure(new Error(nameof(InvalidOperationException),
"PrivateChat not found.")
return Result.Failure<PrivateChat>(
Error.Failure(
nameof(InvalidOperationException),
"PrivateChat not found.")
);
return res;
@@ -1,10 +1,11 @@
using Govor.Domain.Common;
using SmartRes;
namespace Govor.Application.Profiles;
public interface IProfileService
{
public Task<Result<UserProfile>> GetUserProfileAsync(Guid userId);
public Task<Result> SetDescription(string description, Guid userId);
public Task<Result> SetNewIcon(Guid userId, Guid iconId);
public Task<Result<UserProfile, Error>> GetUserProfileAsync(Guid userId);
public Task<Result<Unit, Error>> SetDescription(string description, Guid userId);
public Task<Result<Unit, Error>> SetNewIcon(Guid userId, Guid iconId);
}
+6 -5
View File
@@ -2,6 +2,7 @@
using Govor.Domain.Common;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SmartRes;
namespace Govor.Application.Profiles;
@@ -16,7 +17,7 @@ public class ProfileService : IProfileService
_logger = logger;
}
public async Task<Result<UserProfile>> GetUserProfileAsync(Guid userId)
public async Task<Result<UserProfile, Error>> GetUserProfileAsync(Guid userId)
{
_logger.LogInformation("Getting user {UserId} profile", userId);
@@ -34,13 +35,13 @@ public class ProfileService : IProfileService
if (profile is null)
{
return Result<UserProfile>.Failure(CreateNotFoundError(userId));
return Result.Failure<UserProfile>(CreateNotFoundError(userId));
}
return profile;
}
public async Task<Result> SetDescription(string description, Guid userId)
public async Task<Result<Unit, Error>> SetDescription(string description, Guid userId)
{
_logger.LogInformation("Updating description for user {UserId}", userId);
@@ -56,7 +57,7 @@ public class ProfileService : IProfileService
return Result.Success();
}
public async Task<Result> SetNewIcon(Guid userId, Guid iconId)
public async Task<Result<Unit, Error>> SetNewIcon(Guid userId, Guid iconId)
{
_logger.LogInformation("Updating icon for user {UserId}", userId);
@@ -74,5 +75,5 @@ public class ProfileService : IProfileService
}
private static Error CreateNotFoundError(Guid userId) =>
new("Profile.UserNotFound", $"User with ID {userId} was not found.");
Error.NotFound("Profile.UserNotFound", $"User with ID {userId} was not found.");
}
@@ -1,12 +1,13 @@
using Govor.Domain.Common;
using SmartRes;
namespace Govor.Application.PushNotifications;
public interface IPushNotificationService
{
Task<Result> SendToUserAsync(Guid userId, string title, string body, string channelId, string tag = "", Dictionary<string, string>? data = null);
Task<Result<Unit, Error>> SendToUserAsync(Guid userId, string title, string body, string channelId, string tag = "", Dictionary<string, string>? data = null);
Task<Result> SendToUsersAsync(IEnumerable<Guid> userIds, string title, string body, string channelId, string tag = "", Dictionary<string, string>? data = null);
Task<Result<Unit, Error>> SendToUsersAsync(IEnumerable<Guid> userIds, string title, string body, string channelId, string tag = "", Dictionary<string, string>? data = null);
Task<Result> SendToSessionAsync(Guid sessionId, string title, string body, string channelId, string tag = "", Dictionary<string, string>? data = null);
Task<Result<Unit, Error>> SendToSessionAsync(Guid sessionId, string title, string body, string channelId, string tag = "", Dictionary<string, string>? data = null);
}
@@ -1,14 +1,15 @@
using Govor.Domain.Common;
using SmartRes;
namespace Govor.Application.PushNotifications;
public interface IPushTokenService
{
Task<Result> DeactivateTokenBySessionAsync(Guid sessionId);
Task<Result> DeactivateAllTokensByUserIdAsync(Guid userId);
Task<Result<List<string>>> GetStringsActiveTokensAsync(Guid userId);
Task<Result<List<string>>> GetUsersStringsActiveTokensAsync(IEnumerable<Guid> userIds);
Task<Result<string?>> GetActiveTokenBySessionAsync(Guid sessionId);
Task<Result> RemoveTokensAsync(IEnumerable<string> tokens);
Task<Result> AddOrUpdateTokenAsync(Guid userId, Guid sessionId, string token, string platform);
Task<Result<Unit, Error>> DeactivateTokenBySessionAsync(Guid sessionId);
Task<Result<Unit, Error>> DeactivateAllTokensByUserIdAsync(Guid userId);
Task<Result<List<string>, Error>> GetStringsActiveTokensAsync(Guid userId);
Task<Result<List<string>, Error>> GetUsersStringsActiveTokensAsync(IEnumerable<Guid> userIds);
Task<Result<string?, Error>> GetActiveTokenBySessionAsync(Guid sessionId);
Task<Result<Unit, Error>> RemoveTokensAsync(IEnumerable<string> tokens);
Task<Result<Unit, Error>> AddOrUpdateTokenAsync(Guid userId, Guid sessionId, string token, string platform);
}
@@ -2,6 +2,7 @@ using Govor.Application.Interfaces.PushNotifications.Models;
using Govor.Application.PushNotifications.Providers;
using Govor.Domain.Common;
using Microsoft.Extensions.Logging;
using SmartRes;
namespace Govor.Application.PushNotifications;
@@ -21,7 +22,7 @@ public class PushNotificationService : IPushNotificationService
_logger = logger;
}
public async Task<Result> SendToUserAsync(Guid userId, string title, string body, string channelId, string tag = "", Dictionary<string, string>? data = null)
public async Task<Result<Unit, Error>> SendToUserAsync(Guid userId, string title, string body, string channelId, string tag = "", Dictionary<string, string>? data = null)
{
var resultTokens = await _tokenService.GetStringsActiveTokensAsync(userId);
if (resultTokens.IsFailure)
@@ -37,10 +38,10 @@ public class PushNotificationService : IPushNotificationService
return await SendMulticastInternalAsync(tokens, title, body, channelId, tag, data, userId.ToString());
}
public async Task<Result> SendToUsersAsync(IEnumerable<Guid> userIds, string title, string body, string channelId, string tag = "", Dictionary<string, string>? data = null)
public async Task<Result<Unit, Error>> SendToUsersAsync(IEnumerable<Guid> userIds, string title, string body, string channelId, string tag = "", Dictionary<string, string>? data = null)
{
if (userIds == null || !userIds.Any())
return Result.Failure(new Error("Push.InvalidArgs", "User IDs collection cannot be empty."));
return Result.Failure(Error.Failure("Push.InvalidArgs", "User IDs collection cannot be empty."));
var tokensResult = await _tokenService.GetUsersStringsActiveTokensAsync(userIds);
if (tokensResult.IsFailure)
@@ -53,7 +54,7 @@ public class PushNotificationService : IPushNotificationService
return await SendMulticastInternalAsync(tokens, title, body, channelId, tag, data, "bulk_request");
}
public async Task<Result> SendToSessionAsync(Guid sessionId, string title, string body, string channelId, string tag = "", Dictionary<string, string>? data = null)
public async Task<Result<Unit, Error>> SendToSessionAsync(Guid sessionId, string title, string body, string channelId, string tag = "", Dictionary<string, string>? data = null)
{
var tokenResult = await _tokenService.GetActiveTokenBySessionAsync(sessionId);
if (tokenResult.IsFailure)
@@ -83,7 +84,7 @@ public class PushNotificationService : IPushNotificationService
}
}
private async Task<Result> SendMulticastInternalAsync(List<string> tokens, string title, string body, string channelId, string tag, Dictionary<string, string>? data, string targetInfo)
private async Task<Result<Unit, Error>> SendMulticastInternalAsync(List<string> tokens, string title, string body, string channelId, string tag, Dictionary<string, string>? data, string targetInfo)
{
try
{
@@ -3,6 +3,7 @@ using Govor.Domain.Common;
using Govor.Domain.Models.Users;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SmartRes;
namespace Govor.Application.PushNotifications;
@@ -17,7 +18,7 @@ public class PushTokenService : IPushTokenService
_logger = logger;
}
public async Task<Result> DeactivateTokenBySessionAsync(Guid sessionId)
public async Task<Result<Unit, Error>> DeactivateTokenBySessionAsync(Guid sessionId)
{
try
{
@@ -34,7 +35,7 @@ public class PushTokenService : IPushTokenService
}
}
public async Task<Result> DeactivateAllTokensByUserIdAsync(Guid userId)
public async Task<Result<Unit, Error>> DeactivateAllTokensByUserIdAsync(Guid userId)
{
try
{
@@ -51,7 +52,7 @@ public class PushTokenService : IPushTokenService
}
}
public async Task<Result<List<string>>> GetStringsActiveTokensAsync(Guid userId)
public async Task<Result<List<string>, Error>> GetStringsActiveTokensAsync(Guid userId)
{
try
{
@@ -66,11 +67,11 @@ public class PushTokenService : IPushTokenService
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch active push tokens for user {UserId}", userId);
return Result<List<string>>.Failure(ex);
return Result.Failure<List<string>>(ex);
}
}
public async Task<Result<List<string>>> GetUsersStringsActiveTokensAsync(IEnumerable<Guid> userIds)
public async Task<Result<List<string>, Error>> GetUsersStringsActiveTokensAsync(IEnumerable<Guid> userIds)
{
try
{
@@ -85,11 +86,11 @@ public class PushTokenService : IPushTokenService
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch active push tokens for bulk users");
return Result<List<string>>.Failure(ex);
return Result.Failure<List<string>>(ex);
}
}
public async Task<Result<string?>> GetActiveTokenBySessionAsync(Guid sessionId)
public async Task<Result<string?, Error>> GetActiveTokenBySessionAsync(Guid sessionId)
{
try
{
@@ -104,11 +105,11 @@ public class PushTokenService : IPushTokenService
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch active push token for session {SessionId}", sessionId);
return Result<string?>.Failure(ex);
return Result.Failure<string?>(ex);
}
}
public async Task<Result> RemoveTokensAsync(IEnumerable<string> tokens)
public async Task<Result<Unit, Error>> RemoveTokensAsync(IEnumerable<string> tokens)
{
if (tokens is null || !tokens.Any())
return Result.Success();
@@ -128,11 +129,11 @@ public class PushTokenService : IPushTokenService
}
}
public async Task<Result> AddOrUpdateTokenAsync(Guid userId, Guid sessionId, string token, string platform)
public async Task<Result<Unit, Error>> AddOrUpdateTokenAsync(Guid userId, Guid sessionId, string token, string platform)
{
if (string.IsNullOrWhiteSpace(token))
{
return new Error("PushToken.Empty", "Push token cannot be empty.");
return Result<Unit, Error>.Failure(Error.Failure("PushToken.Empty", "Push token cannot be empty."));
}
var existingToken = await _context.UserPushTokens
@@ -1,11 +1,12 @@
using Govor.Domain.Common;
using Govor.Domain.Models.Users;
using SmartRes;
namespace Govor.Application.Users.UserSessions;
public interface IUserSessionOpener
{
Task<Result<RefreshResult>> OpenSessionAsync(User user, string deviceInfo);
Task<Result<RefreshResult, Error>> OpenSessionAsync(User user, string deviceInfo);
}
@@ -1,8 +1,9 @@
using Govor.Domain.Common;
using SmartRes;
namespace Govor.Application.Users.UserSessions;
public interface IUserSessionReader
{
Task<Result<List<Domain.Models.Users.UserSession>>> GetAllSessionsAsync(Guid userId);
Task<Result<List<Domain.Models.Users.UserSession>, Error>> GetAllSessionsAsync(Guid userId);
}
@@ -1,8 +1,9 @@
using Govor.Domain.Common;
using SmartRes;
namespace Govor.Application.Users.UserSessions;
public interface IUserSessionRefresher
{
Task<Result<RefreshResult>> RefreshTokenAsync(string refreshToken);
Task<Result<RefreshResult, Error>> RefreshTokenAsync(string refreshToken);
}
@@ -1,10 +1,11 @@
using Govor.Domain.Common;
using SmartRes;
namespace Govor.Application.Users.UserSessions;
public interface IUserSessionRevoker
{
Task<Result> CloseSessionByIdAsync(Guid sessionId, Guid userId);
Task<Result> CloseAllSessionsAsync(Guid userId);
Task<Result<Unit, Error>> CloseSessionByIdAsync(Guid sessionId, Guid userId);
Task<Result<Unit, Error>> CloseAllSessionsAsync(Guid userId);
}
@@ -1,9 +1,10 @@
using Govor.Application.Authentication.JWT;
using Govor.Domain;
using Govor.Domain.Common; // Путь к вашему Result и Error
using Govor.Domain.Common;
using Govor.Domain.Models.Users;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SmartRes;
namespace Govor.Application.Users.UserSessions;
@@ -32,7 +33,7 @@ public class UserSessionOpener : IUserSessionOpener
_jwtService = jwtService;
}
public async Task<Result<RefreshResult>> OpenSessionAsync(User user, string deviceInfo)
public async Task<Result<RefreshResult, Error>> OpenSessionAsync(User user, string deviceInfo)
{
_logger.LogInformation("Opening session for user {UserId} on device '{DeviceInfo}'", user.Id, deviceInfo);
@@ -41,7 +42,7 @@ public class UserSessionOpener : IUserSessionOpener
if (result.IsFailure)
{
_logger.LogError("Failed to fetch sessions for user {UserId}: {Error}", user.Id, result.Error.Message);
return Result<RefreshResult>.Failure(result.Error);
return Result.Failure<RefreshResult>(result.Error);
}
var sessions = result.Value;
@@ -67,7 +68,7 @@ public class UserSessionOpener : IUserSessionOpener
catch (Exception ex)
{
_logger.LogError(ex, "Database error while opening session for user {UserId}", user.Id);
return Result<RefreshResult>.Failure(ex);
return Result.Failure<RefreshResult>(ex);
}
}
@@ -3,6 +3,7 @@ using Govor.Domain.Common;
using Govor.Domain.Models.Users;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SmartRes;
namespace Govor.Application.Users.UserSessions;
@@ -17,13 +18,14 @@ public class UserSessionReader : IUserSessionReader
_logger = logger;
}
public async Task<Result<List<UserSession>>> GetAllSessionsAsync(Guid userId)
public async Task<Result<List<UserSession>, Error>> GetAllSessionsAsync(Guid userId)
{
if (userId == Guid.Empty)
{
return Result<List<UserSession>>.Failure(new Error(
"UserSession.InvalidUserId",
"Provided User ID cannot be empty."));
return Result.Failure<List<UserSession>>(Error.Conflict("UserSession.InvalidUserId",
"Provided User ID cannot be empty.")
);
}
_logger.LogInformation("Getting all active sessions for user {UserId}", userId);
@@ -40,7 +42,7 @@ public class UserSessionReader : IUserSessionReader
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch user sessions for user {UserId}", userId);
return Result<List<UserSession>>.Failure(ex);
return Result.Failure<List<UserSession>>(ex);
}
}
}
@@ -4,6 +4,7 @@ using Govor.Domain.Common;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using SmartRes;
namespace Govor.Application.Users.UserSessions;
@@ -29,11 +30,11 @@ public class UserSessionRefresher : IUserSessionRefresher
_context = context;
}
public async Task<Result<RefreshResult>> RefreshTokenAsync(string refreshToken)
public async Task<Result<RefreshResult, Error>> RefreshTokenAsync(string refreshToken)
{
if (string.IsNullOrWhiteSpace(refreshToken))
{
return Result<RefreshResult>.Failure(new Error("Auth.EmptyToken", "Refresh token cannot be empty."));
return Result.Failure<RefreshResult>(Error.Failure("Auth.EmptyToken", "Refresh token cannot be empty."));
}
var hashedToken = _jwtTokenHasher.HashToken(refreshToken);
@@ -48,13 +49,13 @@ public class UserSessionRefresher : IUserSessionRefresher
if (session is null)
{
_logger.LogWarning("Refresh token session not found for hashed token");
return Result<RefreshResult>.Failure(new Error("Auth.InvalidToken", "Invalid refresh token."));
return Result.Failure<RefreshResult>(Error.Failure("Auth.InvalidToken", "Invalid refresh token."));
}
if (session.IsRevoked || session.ExpiresAt <= DateTime.UtcNow)
{
_logger.LogWarning("Attempted to refresh an expired or revoked session: {SessionId}", session.Id);
return Result<RefreshResult>.Failure(new Error("Auth.InvalidToken", "Refresh token is invalid or expired."));
return Result.Failure<RefreshResult>(Error.Failure("Auth.InvalidToken", "Refresh token is invalid or expired."));
}
var newAccessToken = await _jwtService.GenerateAccessTokenAsync(session.User, session.Id);
@@ -74,7 +75,7 @@ public class UserSessionRefresher : IUserSessionRefresher
catch (Exception ex)
{
_logger.LogError(ex, "Database error occurred during token refresh execution");
return Result<RefreshResult>.Failure(ex);
return Result.Failure<RefreshResult>(ex);
}
}
}
@@ -3,6 +3,7 @@ using Govor.Domain;
using Govor.Domain.Common;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SmartRes;
namespace Govor.Application.Users.UserSessions;
@@ -22,7 +23,7 @@ public class UserSessionRevoker : IUserSessionRevoker
_logger = logger;
}
public async Task<Result> CloseSessionByIdAsync(Guid sessionId, Guid userId)
public async Task<Result<Unit, Error>> CloseSessionByIdAsync(Guid sessionId, Guid userId)
{
_logger.LogInformation("Attempting to close session {SessionId} for user {UserId}", sessionId, userId);
@@ -34,9 +35,13 @@ public class UserSessionRevoker : IUserSessionRevoker
if (session is null)
{
_logger.LogWarning("Active session {SessionId} not found or doesn't belong to user {UserId}", sessionId, userId);
return Result.Failure(new Error(
"UserSession.NotFoundOrUnauthorized",
$"Active session {sessionId} for user {userId} was not found."));
return Result.Failure(
Error.NotFound(
"UserSession.NotFoundOrUnauthorized",
$"Active session {sessionId} for user {userId} was not found."
)
);
}
session.IsRevoked = true;
@@ -55,7 +60,7 @@ public class UserSessionRevoker : IUserSessionRevoker
}
}
public async Task<Result> CloseAllSessionsAsync(Guid userId)
public async Task<Result<Unit, Error>> CloseAllSessionsAsync(Guid userId)
{
_logger.LogInformation("Attempting to close all active sessions for user {UserId}", userId);
@@ -25,8 +25,4 @@
<Using Include="NUnit.Framework" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Govor.ConsoleClient\Govor.ConsoleClient.csproj" />
</ItemGroup>
</Project>
-28
View File
@@ -1,28 +0,0 @@
using Govor.ConsoleClient.Services;
using Govor.ConsoleClient.Services.Interfaces;
namespace Govor.ConsoleClient;
public class App
{
private readonly IInputPipeline _inputPipeline;
private readonly ILogger _logger;
public App(IInputPipeline inputPipeline, ILogger logger)
{
_logger = logger;
_inputPipeline = inputPipeline;
}
public async Task RunAsync()
{
_logger.Title("Добро пожаловать в консольный клиент Говор!");
while (true)
{
Console.Write(">> ");
var input = Console.ReadLine();
if (input == null || input.Trim().ToLower() == "exit") break;
await _inputPipeline.ProcessInputAsync(input);
}
}
}
@@ -1,56 +0,0 @@
using System.Reflection;
using Govor.ConsoleClient.Services;
using Govor.ConsoleClient.Services.Interfaces;
using Microsoft.Extensions.DependencyInjection;
namespace Govor.ConsoleClient.Commands;
public class HelpCommand : ICommand
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger;
public HelpCommand(IServiceProvider serviceProvider, ILogger logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
public Task ExecuteAsync(CommandContext context)
{
var dispatcher = _serviceProvider.GetRequiredService<ICommandDispatcher>();
var commands = dispatcher.GetAllCommands();
if (context.Arguments is not null)
{
var command = commands.FirstOrDefault(c =>
(c.GetType().GetCustomAttribute<CommandRouteAttribute>()?.Path.Replace("/", "").ToLower()
?? c.GetType().Name.Replace("Command", "").ToLower()) == context.Arguments.ToLower());
if (command != null)
{
_logger.Info($"{context.Arguments} - {command.LongHelp()}");
}
else
{
_logger.Warn("Unknown command");
}
}
else
{
_logger.Info("Чтобы получить подробную информацию, напишите /help {command}");
foreach (var command in commands)
{
var name = command.GetType().GetCustomAttribute<CommandRouteAttribute>()?.Path.Replace("/", "").ToLower()
?? command.GetType().Name.Replace("Command", "").ToLower();
_logger.Log($"{name} - {command.ShortHelp()}");
}
}
return Task.CompletedTask;
}
public string LongHelp() => "Необходима для получения информации о доступных командах\nЧтобы получить подробную информацию о команде, напишите /help {command}";
public string ShortHelp() => "Необходима для получения информации о доступных командах";
}
-17
View File
@@ -1,17 +0,0 @@
using Govor.ConsoleClient.Services;
namespace Govor.ConsoleClient.Commands;
public interface ICommand
{
Task ExecuteAsync(CommandContext context);
string LongHelp();
string ShortHelp();
}
[AttributeUsage(AttributeTargets.Class)]
public class CommandRouteAttribute : Attribute
{
public string Path { get; }
public CommandRouteAttribute(string path) => Path = path;
}
@@ -1,7 +0,0 @@
namespace Govor.ConsoleClient.Commands;
public interface IInteractiveCommand : ICommand
{
Task HandleInputAsync(string input);
bool IsCompleted { get; }
}
@@ -1,44 +0,0 @@
using Govor.ConsoleClient.Services;
namespace Govor.ConsoleClient.Commands;
[CommandRoute("/send")]
public class SendMessageCommand : IInteractiveCommand
{
private string? _recipient;
private bool _isCompleted;
public bool IsCompleted => _isCompleted;
public Task ExecuteAsync(CommandContext context)
{
Console.WriteLine("Кому вы хотите отправить сообщение?");
return Task.CompletedTask;
}
public async Task HandleInputAsync(string input)
{
if (_recipient == null)
{
_recipient = input;
Console.WriteLine("Введите сообщение:");
}
else
{
var message = input;
Console.WriteLine($"(Отправка '{message}' пользователю '{_recipient}')");
_isCompleted = true;
}
await Task.CompletedTask;
}
public string LongHelp()
{
return "Отпарвка тестовых сообщений существующему юзеру 2";
}
public string ShortHelp()
{
return "Отпарвка тестовых сообщений существующему юзеру";
}
}
@@ -1,46 +0,0 @@
using System.Reflection;
using Govor.ConsoleClient.Commands;
using Govor.ConsoleClient.Services;
using Govor.ConsoleClient.Services.Extensions;
using Govor.ConsoleClient.Services.Interfaces;
using Govor.ConsoleClient.Services.Middleware;
using Microsoft.Extensions.DependencyInjection;
namespace Govor.ConsoleClient;
public static class DependencyInjection
{
public static IServiceProvider Configure()
{
var services = new ServiceCollection();
// Регистрация команд
services.AddCommands();
// Сервисы
services.AddApplicationServices();
services.AddSingleton<CommandContext>();
// Middleware
services.AddSingleton<ICommandMiddleware, ExceptionHandlingMiddleware>();
return services.BuildServiceProvider();
}
public static IServiceCollection AddCommands(this IServiceCollection services)
{
var commandTypes = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => typeof(ICommand).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract);
foreach (var type in commandTypes)
{
services.AddTransient(typeof(ICommand), type);
services.AddTransient(type); // Для прямого внедрения
}
return services;
}
}
@@ -1,29 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>12</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.5" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Govor.Contracts\Govor.Contracts.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Api\" />
<Folder Include="Commands\Auth\" />
<Folder Include="Models\" />
<Folder Include="Utils\" />
</ItemGroup>
</Project>
-18
View File
@@ -1,18 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
namespace Govor.ConsoleClient;
internal class Program
{
//private const string HubBaseUrl = "https://govor-team-govor-88b3.twc1.net/hubs";
static async Task Main()
{
Console.Title = "Govor Console Client";
var serviceProvider = DependencyInjection.Configure();
var app = ActivatorUtilities.CreateInstance<App>(serviceProvider);
await app.RunAsync();
}
}
@@ -1,17 +0,0 @@
using Govor.ConsoleClient.Commands;
namespace Govor.ConsoleClient.Services;
public class CommandContext
{
public string Route { get; }
public string? Arguments { get; }
public ICommand Command { get; }
public CommandContext(string route, string? arguments, ICommand command)
{
Route = route;
Arguments = arguments;
Command = command;
}
}
@@ -1,18 +0,0 @@
using Govor.ConsoleClient.Services.Implementations;
using Govor.ConsoleClient.Services.Interfaces;
using Microsoft.Extensions.DependencyInjection;
namespace Govor.ConsoleClient.Services.Extensions;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
services.AddSingleton<IInputPipeline, InputPipeline>();
services.AddSingleton<ICommandDispatcher, CommandDispatcher>();
services.AddSingleton<IMiddlewarePipeline, MiddlewarePipeline>();
services.AddSingleton<ConsoleLogger>();
services.AddSingleton<ILogger>(sp => sp.GetRequiredService<ConsoleLogger>());
return services;
}
}
@@ -1,44 +0,0 @@
using System.Reflection;
using Govor.ConsoleClient.Commands;
using Govor.ConsoleClient.Services.Interfaces;
namespace Govor.ConsoleClient.Services.Implementations;
public class CommandDispatcher : ICommandDispatcher
{
private readonly Dictionary<string, ICommand> _commands = new();
private readonly ILogger _logger;
private readonly IMiddlewarePipeline _pipeline;
public CommandDispatcher(IEnumerable<ICommand> commands, ILogger logger, IMiddlewarePipeline pipeline)
{
_logger = logger;
_pipeline = pipeline;
foreach (var command in commands)
{
var route = command.GetType().GetCustomAttribute<CommandRouteAttribute>()?.Path.Replace("/","")
?? command.GetType().Name.Replace("Command", "").ToLower();
_commands[route.ToLower()] = command;
}
}
public async Task<ICommand?> DispatchAsync(string input)
{
var args = input.Split(' ', 2);
var cmd = args[0].ToLower();
if (_commands.TryGetValue(cmd, out var command))
{
var context = new CommandContext(cmd, args.Length > 1 ? args[1] : null, command);
await _pipeline.ExecuteAsync(context);
return command;
}
else
{
_logger.Warn("Неизвестная команда. Введите '/help'.");
return null;
}
}
public IEnumerable<ICommand> GetAllCommands() => _commands.Values;
}
@@ -1,47 +0,0 @@
using Govor.ConsoleClient.Services.Interfaces;
namespace Govor.ConsoleClient.Services.Implementations;
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.ResetColor();
Console.WriteLine(message);
}
public void Info(string message)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"[INFO] {message}");
Console.ResetColor();
}
public void Warn(string message)
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"[WARN] {message}");
Console.ResetColor();
}
public void Error(string message)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"[ERROR] {message}");
Console.ResetColor();
}
public void Title(string message)
{
var upper = message.ToUpper();
var length = upper.Length + 6;
var border = new string('=', length);
var padded = $"= {upper} =";
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine(border);
Console.WriteLine(padded);
Console.WriteLine(border);
Console.ResetColor();
}
}
@@ -1,50 +0,0 @@
using Govor.ConsoleClient.Commands;
using Govor.ConsoleClient.Services.Interfaces;
namespace Govor.ConsoleClient.Services.Implementations;
public class InputPipeline : IInputPipeline
{
private readonly ICommandDispatcher _dispatcher;
private readonly ILogger _logger;
private IInteractiveCommand? _activeCommand;
public InputPipeline(ICommandDispatcher dispatcher, ILogger logger)
{
_dispatcher = dispatcher;
_logger = logger;
}
public async Task ProcessInputAsync(string input)
{
if (string.IsNullOrWhiteSpace(input)) return;
if (input.StartsWith("/"))
{
_activeCommand = null;
var commandInput = input[1..];
var result = await _dispatcher.DispatchAsync(commandInput);
// If the command supports interactivity, save it as active
if (result is IInteractiveCommand interactiveCommand && !interactiveCommand.IsCompleted)
{
_activeCommand = interactiveCommand;
}
}
else
{
if (_activeCommand != null)
{
await _activeCommand.HandleInputAsync(input);
if (_activeCommand.IsCompleted)
_activeCommand = null;
}
else
{
_logger.Info($"Введите /help, чтобы узнать доступные команды!");
}
}
}
}
@@ -1,30 +0,0 @@
using Govor.ConsoleClient.Services.Interfaces;
using Govor.ConsoleClient.Services.Middleware;
namespace Govor.ConsoleClient.Services.Implementations;
public delegate Task CommandMiddleware(CommandContext context, Func<Task> next);
public class MiddlewarePipeline : IMiddlewarePipeline
{
private readonly IList<ICommandMiddleware> _middlewares;
public MiddlewarePipeline(IEnumerable<ICommandMiddleware> middlewares)
{
_middlewares = middlewares.ToList();
}
public Task ExecuteAsync(CommandContext context)
{
return InvokeNext(0, context);
}
private Task InvokeNext(int index, CommandContext context)
{
if (index < _middlewares.Count)
{
return _middlewares[index].InvokeAsync(context, () => InvokeNext(index + 1, context));
}
return context.Command.ExecuteAsync(context);
}
}
@@ -1,9 +0,0 @@
using Govor.ConsoleClient.Commands;
namespace Govor.ConsoleClient.Services.Interfaces;
public interface ICommandDispatcher
{
Task<ICommand?> DispatchAsync(string input);
IEnumerable<ICommand> GetAllCommands();
}
@@ -1,6 +0,0 @@
namespace Govor.ConsoleClient.Services.Interfaces;
public interface IInputPipeline
{
Task ProcessInputAsync(string input);
}
@@ -1,10 +0,0 @@
namespace Govor.ConsoleClient.Services.Interfaces;
public interface ILogger
{
void Log(string message);
void Info(string message);
void Warn(string message);
void Error(string message);
void Title(string title);
}
@@ -1,6 +0,0 @@
namespace Govor.ConsoleClient.Services.Interfaces;
public interface IMiddlewarePipeline
{
Task ExecuteAsync(CommandContext context);
}
@@ -1,25 +0,0 @@
using Govor.ConsoleClient.Services.Interfaces;
namespace Govor.ConsoleClient.Services.Middleware;
public class ExceptionHandlingMiddleware : ICommandMiddleware
{
private readonly ILogger _logger;
public ExceptionHandlingMiddleware(ILogger logger)
{
_logger = logger;
}
public async Task InvokeAsync(CommandContext context, Func<Task> next)
{
try
{
await next();
}
catch (Exception ex)
{
_logger.Error($"Произошла ошибка при выполнении команды '{context?.Route}': {ex.Message}");
}
}
}
@@ -1,6 +0,0 @@
namespace Govor.ConsoleClient.Services.Middleware;
public interface ICommandMiddleware
{
Task InvokeAsync(CommandContext context, Func<Task> next);
}
-3
View File
@@ -1,3 +0,0 @@
{
"BaseUrl": "https://govor-team-govor-88b3.twc1.net"
}
+2 -2
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
@@ -11,6 +11,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Features" Version="2.3.0" />
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>
+11 -4
View File
@@ -1,9 +1,16 @@
namespace Govor.Domain.Common;
public record Error(string Code, string Message)
public record Error(string Code, string Message, ErrorType Type, Dictionary<string, string[]>? Errors = null)
{
public static readonly Error None = new(string.Empty, string.Empty);
public static readonly Error Null = new("NULL", "Value cannot be null.");
public static Error NotFound(string code, string message) => new(code, message, ErrorType.NotFound);
public static Error Validation(string code, string message, Dictionary<string, string[]>? errors = null) =>
new(code, message, ErrorType.Validation, errors);
public static Error Conflict(string code, string message) => new(code, message, ErrorType.Conflict);
public static Error Unauthorized(string code, string message) => new(code, message, ErrorType.Unauthorized);
public static Error Forbidden(string code, string message) => new(code, message, ErrorType.Forbidden);
public static Error Failure(string code, string message) => new(code, message, ErrorType.Failure);
public override string ToString() => $"{Code}: {Message}";
}
+11
View File
@@ -0,0 +1,11 @@
namespace Govor.Domain.Common;
public enum ErrorType
{
Failure = 0, // (400 Bad Request)
Validation = 1, // (400 Bad Request / 422 Unprocessable)
NotFound = 2, // (404 Not Found)
Conflict = 3, // (409 Conflict)
Unauthorized = 4, // (401 Unauthorized)
Forbidden = 5 // (403 Forbidden)
}
+14 -43
View File
@@ -1,49 +1,20 @@
using SmartRes;
namespace Govor.Domain.Common;
public class Result
public static class Result
{
protected Result(bool isSuccess, Error error)
{
if (isSuccess && error != Error.None || !isSuccess && error == Error.None)
{
throw new ArgumentException("Invalid error state", nameof(error));
}
// Для методов, возвращающих значение
public static Result<T, Error> Success<T>(T value) => Result<T, Error>.Success(value);
public static Result<T, Error> Failure<T>(Error error) => Result<T, Error>.Failure(error);
IsSuccess = isSuccess;
Error = error;
}
public bool IsSuccess { get; }
public bool IsFailure => !IsSuccess;
public Error Error { get; }
public static Result Success() => new(true, Error.None);
public static Result Failure(Error error) => new(false, error);
// Для методов, которые раньше возвращали просто Result (без T)
public static Result<Unit, Error> Success() => Result<Unit, Error>.Success(Unit.Value);
public static Result<Unit, Error> Failure(Error error) => Result<Unit, Error>.Failure(error);
public static Result Failure(Exception ex) => new(false, new Error(ex.GetType().Name, ex.Message));
public static implicit operator Result(Error error) => Failure(error);
}
public class Result<T> : Result
{
private readonly T? _value;
private Result(T? value, bool isSuccess, Error error) : base(isSuccess, error)
{
_value = value;
}
public T Value => IsSuccess
? _value!
: throw new InvalidOperationException("The value of a failure result cannot be accessed.");
public static Result<T> Success(T value) => new(value, true, Error.None);
public static new Result<T> Failure(Error error) => new(default, false, error);
public static new Result<T> Failure(Exception ex) => new(default, false, new Error(ex.GetType().Name, ex.Message));
public static implicit operator Result<T>(T value) => Success(value);
public static implicit operator Result<T>(Error error) => Failure(error);
public static implicit operator T(Result<T> result) => result.Value;
public static Result<Unit, Error> Failure(Exception ex) =>
Result<Unit, Error>.Failure(new Error(ex.GetType().Name, ex.Message, ErrorType.Failure));
public static Result<T, Error> Failure<T>(Exception ex) =>
Result<T, Error>.Failure(new Error(ex.GetType().Name, ex.Message, ErrorType.Failure));
}
+10 -4
View File
@@ -1,14 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.10" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
<PackageReference Include="Microting.EntityFrameworkCore.MySql" Version="10.0.10" />
</ItemGroup>
<ItemGroup>
<Reference Include="SmartRes">
<HintPath>..\libs\SmartRes.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
+6 -7
View File
@@ -4,8 +4,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Govor.API.Tests", "Govor.AP
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Govor.API", "Govor.API\Govor.API.csproj", "{EA8F272F-4276-438A-9DEA-C58860A440AE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Govor.ConsoleClient", "Govor.ConsoleClient\Govor.ConsoleClient.csproj", "{F4535DC3-BDFB-4EB2-B259-F92B6BBB535B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{4ED5259A-6FB4-4D89-8E6B-4778DC68F7D4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{114F53C1-B0AB-4BA0-9E36-0E811D1B3776}"
@@ -25,6 +23,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Govor.ConsoleClient.Tests",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Govor.Domain", "Govor.Domain\Govor.Domain.csproj", "{868EAA88-A8E8-4508-B264-1AE3576D47C1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartRes", "..\SmartRes\SmartRes\SmartRes.csproj", "{3AB1DEB2-131B-476F-B4B7-7F8F5479D2FC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -39,10 +39,6 @@ Global
{EA8F272F-4276-438A-9DEA-C58860A440AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EA8F272F-4276-438A-9DEA-C58860A440AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EA8F272F-4276-438A-9DEA-C58860A440AE}.Release|Any CPU.Build.0 = Release|Any CPU
{F4535DC3-BDFB-4EB2-B259-F92B6BBB535B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F4535DC3-BDFB-4EB2-B259-F92B6BBB535B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F4535DC3-BDFB-4EB2-B259-F92B6BBB535B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F4535DC3-BDFB-4EB2-B259-F92B6BBB535B}.Release|Any CPU.Build.0 = Release|Any CPU
{4E94907F-BE20-42A6-AB15-637850FEAD11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4E94907F-BE20-42A6-AB15-637850FEAD11}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4E94907F-BE20-42A6-AB15-637850FEAD11}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -67,11 +63,14 @@ Global
{868EAA88-A8E8-4508-B264-1AE3576D47C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{868EAA88-A8E8-4508-B264-1AE3576D47C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{868EAA88-A8E8-4508-B264-1AE3576D47C1}.Release|Any CPU.Build.0 = Release|Any CPU
{3AB1DEB2-131B-476F-B4B7-7F8F5479D2FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3AB1DEB2-131B-476F-B4B7-7F8F5479D2FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3AB1DEB2-131B-476F-B4B7-7F8F5479D2FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3AB1DEB2-131B-476F-B4B7-7F8F5479D2FC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{15031CBD-F319-4755-BA91-B86F20BD8E37} = {4ED5259A-6FB4-4D89-8E6B-4778DC68F7D4}
{EA8F272F-4276-438A-9DEA-C58860A440AE} = {114F53C1-B0AB-4BA0-9E36-0E811D1B3776}
{F4535DC3-BDFB-4EB2-B259-F92B6BBB535B} = {114F53C1-B0AB-4BA0-9E36-0E811D1B3776}
{4E94907F-BE20-42A6-AB15-637850FEAD11} = {114F53C1-B0AB-4BA0-9E36-0E811D1B3776}
{FC5EDCA8-FD58-4078-8FB1-2BDBB2F6CA3E} = {114F53C1-B0AB-4BA0-9E36-0E811D1B3776}
{F56A64DF-2938-4BE0-83F2-B86429F19259} = {4ED5259A-6FB4-4D89-8E6B-4778DC68F7D4}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "8.0.0",
"version": "10.0.0",
"rollForward": "latestMajor",
"allowPrerelease": true
}
BIN
View File
Binary file not shown.