0
0
mirror of https://github.com/alex289/CleanArchitecture.git synced 2025-06-30 02:31:08 +00:00

Add gRPC and fix some warnings

This commit is contained in:
Alexander Konietzko 2023-03-13 20:34:40 +01:00
parent 2c8bf95254
commit f645b2cc8f
No known key found for this signature in database
GPG Key ID: BA6905F37AEC2B5B
55 changed files with 391 additions and 65 deletions

View File

@ -24,6 +24,7 @@
<ItemGroup>
<ProjectReference Include="..\CleanArchitecture.Application\CleanArchitecture.Application.csproj" />
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj" />
<ProjectReference Include="..\CleanArchitecture.gRPC\CleanArchitecture.gRPC.csproj" />
<ProjectReference Include="..\CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj" />
</ItemGroup>

View File

@ -56,7 +56,7 @@ public class ApiController : ControllerBase
return GetErrorStatusCode();
}
protected HttpStatusCode GetErrorStatusCode()
private HttpStatusCode GetErrorStatusCode()
{
if (_notifications.GetNotifications().Any(n => n.Code == ErrorCodes.ObjectNotFound))
{

View File

@ -29,9 +29,11 @@ public class UserController : ApiController
}
[HttpGet("{id}")]
public async Task<IActionResult> GetUserByIdAsync([FromRoute] Guid id)
public async Task<IActionResult> GetUserByIdAsync(
[FromRoute] Guid id,
[FromQuery] bool isDeleted = false)
{
var user = await _userService.GetUserByUserIdAsync(id);
var user = await _userService.GetUserByUserIdAsync(id, isDeleted);
return Response(user);
}

View File

@ -1,5 +1,6 @@
using CleanArchitecture.Application.Extensions;
using CleanArchitecture.Domain.Extensions;
using CleanArchitecture.gRPC;
using CleanArchitecture.Infrastructure.Database;
using CleanArchitecture.Infrastructure.Extensions;
using Microsoft.AspNetCore.Builder;
@ -10,6 +11,7 @@ using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddGrpc();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
@ -43,6 +45,7 @@ app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.MapGrpcService<UsersApiImplementation>();
using (IServiceScope scope = app.Services.CreateScope())
{

View File

@ -2,7 +2,6 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>

View File

@ -1,3 +1,5 @@
using System;
using System.Linq;
using CleanArchitecture.Application.Queries.Users.GetAll;
using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Domain.Interfaces.Repositories;
@ -8,7 +10,7 @@ namespace CleanArchitecture.Application.Tests.Fixtures.Queries.Users;
public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture
{
public Mock<IUserRepository> UserRepository { get; }
private Mock<IUserRepository> UserRepository { get; }
public GetAllUsersQueryHandler Handler { get; }
public Guid ExistingUserId { get; } = Guid.NewGuid();

View File

@ -1,3 +1,5 @@
using System;
using System.Linq;
using CleanArchitecture.Application.Queries.Users.GetUserById;
using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Domain.Interfaces.Repositories;
@ -8,7 +10,7 @@ namespace CleanArchitecture.Application.Tests.Fixtures.Queries.Users;
public sealed class GetUserByIdTestFixture : QueryHandlerBaseFixture
{
public Mock<IUserRepository> UserRepository { get; }
private Mock<IUserRepository> UserRepository { get; }
public GetUserByIdQueryHandler Handler { get; }
public Guid ExistingUserId { get; } = Guid.NewGuid();

View File

@ -1,3 +1,5 @@
using System.Linq;
using System.Threading.Tasks;
using CleanArchitecture.Application.Tests.Fixtures.Queries.Users;
using FluentAssertions;
using Xunit;
@ -18,7 +20,7 @@ public sealed class GetAllUsersQueryHandlerTests
default);
_fixture.VerifyNoDomainNotification();
result.Should().NotBeNull();
result.Should().ContainSingle();
result.FirstOrDefault()!.Id.Should().Be(_fixture.ExistingUserId);

View File

@ -1,3 +1,5 @@
using System;
using System.Threading.Tasks;
using CleanArchitecture.Application.Queries.Users.GetUserById;
using CleanArchitecture.Application.Tests.Fixtures.Queries.Users;
using CleanArchitecture.Domain.Errors;
@ -16,7 +18,7 @@ public sealed class GetUserByIdQueryHandlerTests
_fixture.SetupUserAsync();
var result = await _fixture.Handler.Handle(
new(_fixture.ExistingUserId),
new(_fixture.ExistingUserId, false),
default);
_fixture.VerifyNoDomainNotification();
@ -30,7 +32,7 @@ public sealed class GetUserByIdQueryHandlerTests
{
_fixture.SetupUserAsync();
var request = new GetUserByIdQuery(Guid.NewGuid());
var request = new GetUserByIdQuery(Guid.NewGuid(), false);
var result = await _fixture.Handler.Handle(
request,
default);

View File

@ -3,7 +3,6 @@ using CleanArchitecture.Application.Interfaces;
using CleanArchitecture.Application.Queries.Users.GetAll;
using CleanArchitecture.Application.Queries.Users.GetUserById;
using CleanArchitecture.Application.Services;
using CleanArchitecture.Application.ViewModels;
using CleanArchitecture.Application.ViewModels.Users;
using MediatR;
using Microsoft.Extensions.DependencyInjection;

View File

@ -7,7 +7,7 @@ namespace CleanArchitecture.Application.Interfaces;
public interface IUserService
{
public Task<UserViewModel?> GetUserByUserIdAsync(Guid userId);
public Task<UserViewModel?> GetUserByUserIdAsync(Guid userId, bool isDeleted);
public Task<IEnumerable<UserViewModel>> GetAllUsersAsync();
public Task<Guid> CreateUserAsync(CreateUserViewModel user);
public Task UpdateUserAsync(UpdateUserViewModel user);

View File

@ -1,8 +1,7 @@
using System.Collections.Generic;
using CleanArchitecture.Application.ViewModels;
using CleanArchitecture.Application.ViewModels.Users;
using MediatR;
namespace CleanArchitecture.Application.Queries.Users.GetAll;
public sealed record GetAllUsersQuery() : IRequest<IEnumerable<UserViewModel>>;
public sealed record GetAllUsersQuery : IRequest<IEnumerable<UserViewModel>>;

View File

@ -2,7 +2,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CleanArchitecture.Application.ViewModels;
using CleanArchitecture.Application.ViewModels.Users;
using CleanArchitecture.Domain.Interfaces.Repositories;
using MediatR;

View File

@ -1,8 +1,7 @@
using System;
using CleanArchitecture.Application.ViewModels;
using CleanArchitecture.Application.ViewModels.Users;
using MediatR;
namespace CleanArchitecture.Application.Queries.Users.GetUserById;
public sealed record GetUserByIdQuery(Guid UserId) : IRequest<UserViewModel?>;
public sealed record GetUserByIdQuery(Guid UserId, bool IsDeleted) : IRequest<UserViewModel?>;

View File

@ -1,7 +1,6 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CleanArchitecture.Application.ViewModels;
using CleanArchitecture.Application.ViewModels.Users;
using CleanArchitecture.Domain.Errors;
using CleanArchitecture.Domain.Interfaces;
@ -27,7 +26,9 @@ public sealed class GetUserByIdQueryHandler :
{
var user = _userRepository
.GetAllNoTracking()
.FirstOrDefault(x => x.Id == request.UserId && !x.Deleted);
.FirstOrDefault(x =>
x.Id == request.UserId &&
x.Deleted == request.IsDeleted);
if (user == null)
{

View File

@ -21,9 +21,9 @@ public sealed class UserService : IUserService
_bus = bus;
}
public async Task<UserViewModel?> GetUserByUserIdAsync(Guid userId)
public async Task<UserViewModel?> GetUserByUserIdAsync(Guid userId, bool isDeleted)
{
return await _bus.QueryAsync(new GetUserByIdQuery(userId));
return await _bus.QueryAsync(new GetUserByIdQuery(userId, isDeleted));
}
public async Task<IEnumerable<UserViewModel>> GetAllUsersAsync()

View File

@ -2,7 +2,6 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>

View File

@ -1,3 +1,4 @@
using System;
using CleanArchitecture.Domain.Commands.Users.CreateUser;
using CleanArchitecture.Domain.Errors;
using CleanArchitecture.Domain.Events.User;

View File

@ -1,3 +1,4 @@
using System;
using CleanArchitecture.Domain.Commands.Users.CreateUser;
using CleanArchitecture.Domain.Interfaces.Repositories;
using Moq;

View File

@ -1,3 +1,4 @@
using System;
using CleanArchitecture.Domain.Commands.Users.CreateUser;
using CleanArchitecture.Domain.Errors;
using Xunit;

View File

@ -1,3 +1,4 @@
using System;
using CleanArchitecture.Domain.Commands.Users.DeleteUser;
using CleanArchitecture.Domain.Errors;
using CleanArchitecture.Domain.Events.User;

View File

@ -1,3 +1,4 @@
using System;
using CleanArchitecture.Domain.Commands.Users.DeleteUser;
using CleanArchitecture.Domain.Interfaces.Repositories;
using Moq;

View File

@ -1,3 +1,4 @@
using System;
using CleanArchitecture.Domain.Commands.Users.DeleteUser;
using CleanArchitecture.Domain.Errors;
using Xunit;

View File

@ -1,3 +1,5 @@
using System;
using System.Threading.Tasks;
using CleanArchitecture.Domain.Commands.Users.UpdateUser;
using CleanArchitecture.Domain.Errors;
using CleanArchitecture.Domain.Events.User;
@ -31,7 +33,7 @@ public sealed class UpdateUserCommandHandlerTests
[Fact]
public async Task Should_Not_Update_Non_Existing_User()
{
var user = _fixture.SetupUser();
_fixture.SetupUser();
var command = new UpdateUserCommand(
Guid.NewGuid(),

View File

@ -1,3 +1,4 @@
using System;
using CleanArchitecture.Domain.Commands.Users.UpdateUser;
using CleanArchitecture.Domain.Interfaces.Repositories;
using Moq;

View File

@ -1,3 +1,4 @@
using System;
using CleanArchitecture.Domain.Commands.Users.UpdateUser;
using CleanArchitecture.Domain.Errors;
using Xunit;

View File

@ -1,3 +1,4 @@
using System;
using System.Linq.Expressions;
using CleanArchitecture.Domain.Interfaces;
using CleanArchitecture.Domain.Notifications;
@ -7,9 +8,9 @@ namespace CleanArchitecture.Domain.Tests;
public class CommandHandlerFixtureBase
{
public Mock<IMediatorHandler> Bus { get; protected set; }
public Mock<IUnitOfWork> UnitOfWork { get; protected set; }
public Mock<DomainNotificationHandler> NotificationHandler { get; protected set; }
protected Mock<IMediatorHandler> Bus { get; }
protected Mock<IUnitOfWork> UnitOfWork { get; }
protected Mock<DomainNotificationHandler> NotificationHandler { get; }
protected CommandHandlerFixtureBase()
{

View File

@ -1,3 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using CleanArchitecture.Domain.Commands;
using FluentAssertions;
using FluentValidation;
@ -8,7 +10,7 @@ public class ValidationTestBase<TCommand, TValidation>
where TCommand : CommandBase
where TValidation: AbstractValidator<TCommand>
{
protected readonly TValidation _validation;
private readonly TValidation _validation;
protected ValidationTestBase(TValidation validation)
{

View File

@ -4,7 +4,7 @@ namespace CleanArchitecture.Domain.Commands.Users.CreateUser;
public sealed class CreateUserCommand : CommandBase
{
private static readonly CreateUserCommandValidation _validation = new();
private readonly CreateUserCommandValidation _validation = new();
public Guid UserId { get; }
public string Email { get; }

View File

@ -4,7 +4,7 @@ namespace CleanArchitecture.Domain.Commands.Users.DeleteUser;
public sealed class DeleteUserCommand : CommandBase
{
private static readonly DeleteUserCommandValidation _validation = new();
private readonly DeleteUserCommandValidation _validation = new();
public Guid UserId { get; }

View File

@ -2,7 +2,6 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>

View File

@ -35,7 +35,7 @@ public sealed class DomainNotificationHandlerTests
var domainNotification = new DomainNotification(key, value, code);
var domainNotificationHandler = new DomainNotificationHandler();
domainNotificationHandler.Handle(domainNotification, default);
domainNotificationHandler.Handle(domainNotification);
domainNotificationHandler.GetNotifications().Should().HaveCount(1);
}

View File

@ -1,4 +1,5 @@
using CleanArchitecture.Domain.Notifications;
using System;
using CleanArchitecture.Domain.Notifications;
using FluentAssertions;
using Xunit;

View File

@ -1,3 +1,6 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using CleanArchitecture.Domain.Commands.Users.DeleteUser;
using CleanArchitecture.Domain.Events.User;
using CleanArchitecture.Domain.Notifications;

View File

@ -1,3 +1,6 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using CleanArchitecture.Infrastructure.Database;
using CleanArchitecture.Infrastructure.Tests.Fixtures;
using FluentAssertions;
@ -37,7 +40,7 @@ public sealed class UnitOfWorkTests
dbContextMock
.Setup(x => x.SaveChangesAsync(CancellationToken.None))
.Throws(new DbUpdateException("Boom", new System.Exception("it broke")));
.Throws(new DbUpdateException("Boom", new Exception("it broke")));
var unitOfWork = UnitOfWorkTestFixture.GetUnitOfWork(dbContextMock.Object, loggerMock.Object);
@ -59,8 +62,8 @@ public sealed class UnitOfWorkTests
var unitOfWork = UnitOfWorkTestFixture.GetUnitOfWork(dbContextMock.Object, loggerMock.Object);
Func<Task> knalltAction = async () => await unitOfWork.CommitAsync();
Func<Task> throwsAction = async () => await unitOfWork.CommitAsync();
await knalltAction.Should().ThrowAsync<Exception>();
await throwsAction.Should().ThrowAsync<Exception>();
}
}

View File

@ -10,23 +10,23 @@ namespace CleanArchitecture.Infrastructure.Repositories;
public class BaseRepository<TEntity> : IRepository<TEntity> where TEntity : Entity
{
protected readonly DbContext _dbContext;
protected readonly DbSet<TEntity> _dbSet;
private readonly DbContext _dbContext;
protected readonly DbSet<TEntity> DbSet;
public BaseRepository(DbContext context)
protected BaseRepository(DbContext context)
{
_dbContext = context;
_dbSet = _dbContext.Set<TEntity>();
DbSet = _dbContext.Set<TEntity>();
}
public void Add(TEntity entity)
{
_dbSet.Add(entity);
DbSet.Add(entity);
}
public void AddRange(IEnumerable<TEntity> entities)
{
_dbSet.AddRange(entities);
DbSet.AddRange(entities);
}
public void Dispose()
@ -37,17 +37,17 @@ public class BaseRepository<TEntity> : IRepository<TEntity> where TEntity : Enti
public virtual IQueryable<TEntity> GetAll()
{
return _dbSet;
return DbSet;
}
public virtual IQueryable<TEntity> GetAllNoTracking()
{
return _dbSet.AsNoTracking();
return DbSet.AsNoTracking();
}
public virtual async Task<TEntity?> GetByIdAsync(Guid id)
{
return await _dbSet.FindAsync(id);
return await DbSet.FindAsync(id);
}
public int SaveChanges()
@ -65,24 +65,24 @@ public class BaseRepository<TEntity> : IRepository<TEntity> where TEntity : Enti
public virtual void Update(TEntity entity)
{
_dbSet.Update(entity);
DbSet.Update(entity);
}
public Task<bool> ExistsAsync(Guid id)
{
return _dbSet.AnyAsync(entity => entity.Id == id);
return DbSet.AnyAsync(entity => entity.Id == id);
}
public void Remove(TEntity entity, bool hardDelete = false)
{
if (hardDelete)
{
_dbSet.Remove(entity);
DbSet.Remove(entity);
}
else
{
entity.Delete();
_dbSet.Update(entity);
DbSet.Update(entity);
}
}

View File

@ -14,6 +14,6 @@ public sealed class UserRepository : BaseRepository<User>, IUserRepository
public async Task<User?> GetByEmailAsync(string email)
{
return await _dbSet.SingleOrDefaultAsync(user => user.Email == email);
return await DbSet.SingleOrDefaultAsync(user => user.Email == email);
}
}

View File

@ -6,7 +6,7 @@ using Microsoft.Extensions.Logging;
namespace CleanArchitecture.Infrastructure;
public class UnitOfWork<TContext> : IUnitOfWork where TContext : DbContext
public sealed class UnitOfWork<TContext> : IUnitOfWork where TContext : DbContext
{
private readonly TContext _context;
private readonly ILogger<UnitOfWork<TContext>> _logger;
@ -34,10 +34,11 @@ public class UnitOfWork<TContext> : IUnitOfWork where TContext : DbContext
public void Dispose()
{
Dispose(true);
// ReSharper disable once GCSuppressFinalizeForTypeWithoutDestructor
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
private void Dispose(bool disposing)
{
if (disposing)
{

View File

@ -2,7 +2,6 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>

View File

@ -1,4 +1,8 @@
using System.Net;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using CleanArchitecture.Application.ViewModels.Users;
using CleanArchitecture.IntegrationTests.Extensions;
using CleanArchitecture.IntegrationTests.Fixtures;
@ -8,7 +12,7 @@ using Xunit.Priority;
namespace CleanArchitecture.IntegrationTests.Controller;
[Collection("Integrationtests")]
[Collection("IntegrationTests")]
[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
public sealed class UserControllerTests : IClassFixture<UserTestFixture>
{
@ -122,7 +126,7 @@ public sealed class UserControllerTests : IClassFixture<UserTestFixture>
message?.Data.Should().NotBeNull();
var content = message!.Data!;
var content = message!.Data!.ToList();
content.Should().ContainSingle();
content.First().Id.Should().Be(_fixture.CreatedUserId);
@ -142,7 +146,7 @@ public sealed class UserControllerTests : IClassFixture<UserTestFixture>
message?.Data.Should().NotBeEmpty();
var content = message!.Data!;
var content = message!.Data;
content.Should().Be(_fixture.CreatedUserId);
}

View File

@ -1,4 +1,7 @@
using System.Data.Common;
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Linq;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
@ -17,7 +20,7 @@ public static class FunctionalTestsServiceCollectionExtensions
services.AddScoped(p =>
DbContextOptionsFactory<TContext>(
p,
(sp, options) => options
(_, options) => options
.ConfigureWarnings(b => b.Log(CoreEventId.ManyServiceProvidersCreatedWarning))
.UseLazyLoadingProxies()
.UseSqlite(connection)));
@ -35,7 +38,7 @@ public static class FunctionalTestsServiceCollectionExtensions
builder.UseApplicationServiceProvider(applicationServiceProvider);
optionsAction?.Invoke(applicationServiceProvider, builder);
optionsAction.Invoke(applicationServiceProvider, builder);
return builder.Options;
}

View File

@ -1,12 +1,14 @@
using System.Text;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using CleanArchitecture.Api.Models;
namespace CleanArchitecture.IntegrationTests.Extensions;
public static class HttpExensions
public static class HttpExtensions
{
public static JsonSerializerOptions JsonSerializerOptions = new()
private static readonly JsonSerializerOptions JsonSerializerOptions = new()
{
PropertyNameCaseInsensitive = true,
};

View File

@ -1,4 +1,7 @@
using CleanArchitecture.Infrastructure.Database;
using System;
using System.IO;
using System.Net.Http;
using CleanArchitecture.Infrastructure.Database;
using CleanArchitecture.IntegrationTests.Infrastructure;
using Microsoft.Extensions.DependencyInjection;

View File

@ -1,4 +1,6 @@
namespace CleanArchitecture.IntegrationTests.Fixtures;
using System;
namespace CleanArchitecture.IntegrationTests.Fixtures;
public sealed class UserTestFixture : TestFixtureBase
{

View File

@ -1,4 +1,5 @@
using CleanArchitecture.Infrastructure.Database;
using System;
using CleanArchitecture.Infrastructure.Database;
using CleanArchitecture.Infrastructure.Extensions;
using CleanArchitecture.IntegrationTests.Extensions;
using Microsoft.AspNetCore.Hosting;

View File

@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Remove="Users\Models.proto"/>
<None Remove="Users\UsersApi.proto"/>
</ItemGroup>
<ItemGroup>
<Protobuf Include="Users\Models.proto" GrpcServices="Both"/>
<Protobuf Include="Users\UsersApi.proto" GrpcServices="Both"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.22.1" />
<PackageReference Include="Google.Protobuf.Tools" Version="3.22.1" />
<PackageReference Include="Grpc.AspNetCore" Version="2.51.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,11 @@
syntax = "proto3";
option csharp_namespace = "CleanArchitecture.Proto.Users";
message GrpcUser {
string id = 1;
string firstName = 3;
string lastName = 4;
string email = 5;
bool isDeleted = 6;
}

View File

@ -0,0 +1,17 @@
syntax = "proto3";
option csharp_namespace = "CleanArchitecture.Proto.Users";
import "Users/Models.proto";
service UsersApi {
rpc GetByIds(GetByIdsRequest) returns (GetByIdsResult);
}
message GetByIdsResult {
repeated GrpcUser users = 1;
}
message GetByIdsRequest {
repeated string ids = 1;
}

View File

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="MockQueryable.Moq" Version="7.0.0" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj" />
<ProjectReference Include="..\CleanArchitecture.gRPC\CleanArchitecture.gRPC.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Domain.Interfaces.Repositories;
using MockQueryable.Moq;
using Moq;
namespace CleanArchitecture.gRPC.Tests.Fixtures;
public sealed class UserTestsFixture
{
private Mock<IUserRepository> UserRepository { get; } = new ();
public UsersApiImplementation UsersApiImplementation { get; }
public IEnumerable<User> ExistingUsers { get; }
public UserTestsFixture()
{
ExistingUsers = new List<User>()
{
new (Guid.NewGuid(), "test@test.de", "Test First Name", "Test Last Name"),
new (Guid.NewGuid(), "email@Email.de", "Email First Name", "Email Last Name"),
new (Guid.NewGuid(), "user@user.de", "User First Name", "User Last Name"),
};
var queryable = ExistingUsers.AsQueryable().BuildMock();
UserRepository
.Setup(repository => repository.GetAllNoTracking())
.Returns(queryable);
UsersApiImplementation = new UsersApiImplementation(UserRepository.Object);
}
}

View File

@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CleanArchitecture.gRPC.Tests.Fixtures;
using CleanArchitecture.Proto.Users;
using FluentAssertions;
using Xunit;
namespace CleanArchitecture.gRPC.Tests.Users;
public sealed class GetUsersByIdsTests : IClassFixture<UserTestsFixture>
{
private readonly UserTestsFixture _fixture;
public GetUsersByIdsTests(UserTestsFixture fixture)
{
_fixture = fixture;
}
[Fact]
public async Task Should_Get_Empty_List_If_No_Ids_Are_Given()
{
var result = await _fixture.UsersApiImplementation.GetByIds(
SetupRequest(Enumerable.Empty<Guid>()),
null!);
result.Users.Should().HaveCount(0);
}
[Fact]
public async Task Should_Get_Requested_Asked_Ids()
{
var nonExistingId = Guid.NewGuid();
var ids = _fixture.ExistingUsers
.Take(2)
.Select(user => user.Id)
.ToList();
ids.Add(nonExistingId);
var result = await _fixture.UsersApiImplementation.GetByIds(
SetupRequest(ids),
null!);
result.Users.Should().HaveCount(2);
foreach (var user in result.Users)
{
var userId = Guid.Parse(user.Id);
userId.Should().NotBe(nonExistingId);
var mockUser = _fixture.ExistingUsers.First(u => u.Id == userId);
mockUser.Should().NotBeNull();
user.Email.Should().Be(mockUser.Email);
user.FirstName.Should().Be(mockUser.GivenName);
user.LastName.Should().Be(mockUser.Surname);
}
}
private static GetByIdsRequest SetupRequest(IEnumerable<Guid> ids)
{
var request = new GetByIdsRequest();
request.Ids.AddRange(ids.Select(id => id.ToString()));
request.Ids.Add("Not a guid");
return request;
}
}

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj" />
<ProjectReference Include="..\CleanArchitecture.Proto\CleanArchitecture.Proto.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.3" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CleanArchitecture.Domain.Interfaces.Repositories;
using CleanArchitecture.Proto.Users;
using Grpc.Core;
using Microsoft.EntityFrameworkCore;
namespace CleanArchitecture.gRPC;
public sealed class UsersApiImplementation : UsersApi.UsersApiBase
{
private readonly IUserRepository _userRepository;
public UsersApiImplementation(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public override async Task<GetByIdsResult> GetByIds(
GetByIdsRequest request,
ServerCallContext context)
{
var idsAsGuids = new List<Guid>(request.Ids.Count);
foreach (var id in request.Ids)
{
if (Guid.TryParse(id, out var parsed))
{
idsAsGuids.Add(parsed);
}
}
var users = await _userRepository
.GetAllNoTracking()
.Where(user => idsAsGuids.Contains(user.Id))
.Select(user => new GrpcUser
{
Id = user.Id.ToString(),
Email = user.Email,
FirstName = user.GivenName,
LastName = user.Surname,
IsDeleted = user.Deleted
})
.ToListAsync();
var result = new GetByIdsResult();
result.Users.AddRange(users);
return result;
}
}

View File

@ -16,6 +16,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Infrastru
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.IntegrationTests", "CleanArchitecture.IntegrationTests\CleanArchitecture.IntegrationTests.csproj", "{39732BD4-909F-410C-8737-1F9FE3E269A7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.gRPC", "CleanArchitecture.gRPC\CleanArchitecture.gRPC.csproj", "{7A6353A9-B60C-4B13-A849-D21B315047EE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Proto", "CleanArchitecture.Proto\CleanArchitecture.Proto.csproj", "{5F978903-7A7A-45C2-ABE0-C2906ECD326B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.gRPC.Tests", "CleanArchitecture.gRPC.Tests\CleanArchitecture.gRPC.Tests.csproj", "{E3A836DD-85DB-44FD-BC19-DDFE111D9EB0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -54,5 +60,17 @@ Global
{39732BD4-909F-410C-8737-1F9FE3E269A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{39732BD4-909F-410C-8737-1F9FE3E269A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{39732BD4-909F-410C-8737-1F9FE3E269A7}.Release|Any CPU.Build.0 = Release|Any CPU
{7A6353A9-B60C-4B13-A849-D21B315047EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7A6353A9-B60C-4B13-A849-D21B315047EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7A6353A9-B60C-4B13-A849-D21B315047EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7A6353A9-B60C-4B13-A849-D21B315047EE}.Release|Any CPU.Build.0 = Release|Any CPU
{5F978903-7A7A-45C2-ABE0-C2906ECD326B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5F978903-7A7A-45C2-ABE0-C2906ECD326B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5F978903-7A7A-45C2-ABE0-C2906ECD326B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5F978903-7A7A-45C2-ABE0-C2906ECD326B}.Release|Any CPU.Build.0 = Release|Any CPU
{E3A836DD-85DB-44FD-BC19-DDFE111D9EB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E3A836DD-85DB-44FD-BC19-DDFE111D9EB0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E3A836DD-85DB-44FD-BC19-DDFE111D9EB0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E3A836DD-85DB-44FD-BC19-DDFE111D9EB0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -1,3 +1,4 @@
- Remove warnings and apply suggestions
- Add gRPC support
- Add authentication and authorization
- Utility integration tests
- Queryhandler tests for deleted users