0
0
mirror of https://github.com/alex289/CleanArchitecture.git synced 2025-07-05 05:23:57 +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> <ItemGroup>
<ProjectReference Include="..\CleanArchitecture.Application\CleanArchitecture.Application.csproj" /> <ProjectReference Include="..\CleanArchitecture.Application\CleanArchitecture.Application.csproj" />
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj" /> <ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj" />
<ProjectReference Include="..\CleanArchitecture.gRPC\CleanArchitecture.gRPC.csproj" />
<ProjectReference Include="..\CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj" /> <ProjectReference Include="..\CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>

View File

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

View File

@ -29,9 +29,11 @@ public class UserController : ApiController
} }
[HttpGet("{id}")] [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); return Response(user);
} }

View File

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

View File

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

View File

@ -1,3 +1,5 @@
using System;
using System.Linq;
using CleanArchitecture.Application.Queries.Users.GetAll; using CleanArchitecture.Application.Queries.Users.GetAll;
using CleanArchitecture.Domain.Entities; using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Domain.Interfaces.Repositories; using CleanArchitecture.Domain.Interfaces.Repositories;
@ -8,7 +10,7 @@ namespace CleanArchitecture.Application.Tests.Fixtures.Queries.Users;
public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture public sealed class GetAllUsersTestFixture : QueryHandlerBaseFixture
{ {
public Mock<IUserRepository> UserRepository { get; } private Mock<IUserRepository> UserRepository { get; }
public GetAllUsersQueryHandler Handler { get; } public GetAllUsersQueryHandler Handler { get; }
public Guid ExistingUserId { get; } = Guid.NewGuid(); 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.Application.Queries.Users.GetUserById;
using CleanArchitecture.Domain.Entities; using CleanArchitecture.Domain.Entities;
using CleanArchitecture.Domain.Interfaces.Repositories; using CleanArchitecture.Domain.Interfaces.Repositories;
@ -8,7 +10,7 @@ namespace CleanArchitecture.Application.Tests.Fixtures.Queries.Users;
public sealed class GetUserByIdTestFixture : QueryHandlerBaseFixture public sealed class GetUserByIdTestFixture : QueryHandlerBaseFixture
{ {
public Mock<IUserRepository> UserRepository { get; } private Mock<IUserRepository> UserRepository { get; }
public GetUserByIdQueryHandler Handler { get; } public GetUserByIdQueryHandler Handler { get; }
public Guid ExistingUserId { get; } = Guid.NewGuid(); 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 CleanArchitecture.Application.Tests.Fixtures.Queries.Users;
using FluentAssertions; using FluentAssertions;
using Xunit; using Xunit;
@ -18,7 +20,7 @@ public sealed class GetAllUsersQueryHandlerTests
default); default);
_fixture.VerifyNoDomainNotification(); _fixture.VerifyNoDomainNotification();
result.Should().NotBeNull(); result.Should().NotBeNull();
result.Should().ContainSingle(); result.Should().ContainSingle();
result.FirstOrDefault()!.Id.Should().Be(_fixture.ExistingUserId); 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.Queries.Users.GetUserById;
using CleanArchitecture.Application.Tests.Fixtures.Queries.Users; using CleanArchitecture.Application.Tests.Fixtures.Queries.Users;
using CleanArchitecture.Domain.Errors; using CleanArchitecture.Domain.Errors;
@ -16,7 +18,7 @@ public sealed class GetUserByIdQueryHandlerTests
_fixture.SetupUserAsync(); _fixture.SetupUserAsync();
var result = await _fixture.Handler.Handle( var result = await _fixture.Handler.Handle(
new(_fixture.ExistingUserId), new(_fixture.ExistingUserId, false),
default); default);
_fixture.VerifyNoDomainNotification(); _fixture.VerifyNoDomainNotification();
@ -30,7 +32,7 @@ public sealed class GetUserByIdQueryHandlerTests
{ {
_fixture.SetupUserAsync(); _fixture.SetupUserAsync();
var request = new GetUserByIdQuery(Guid.NewGuid()); var request = new GetUserByIdQuery(Guid.NewGuid(), false);
var result = await _fixture.Handler.Handle( var result = await _fixture.Handler.Handle(
request, request,
default); default);

View File

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

View File

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

View File

@ -1,8 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using CleanArchitecture.Application.ViewModels;
using CleanArchitecture.Application.ViewModels.Users; using CleanArchitecture.Application.ViewModels.Users;
using MediatR; using MediatR;
namespace CleanArchitecture.Application.Queries.Users.GetAll; 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.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CleanArchitecture.Application.ViewModels;
using CleanArchitecture.Application.ViewModels.Users; using CleanArchitecture.Application.ViewModels.Users;
using CleanArchitecture.Domain.Interfaces.Repositories; using CleanArchitecture.Domain.Interfaces.Repositories;
using MediatR; using MediatR;

View File

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

View File

@ -21,9 +21,9 @@ public sealed class UserService : IUserService
_bus = bus; _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() public async Task<IEnumerable<UserViewModel>> GetAllUsersAsync()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
using CleanArchitecture.Domain.Notifications; using System;
using CleanArchitecture.Domain.Notifications;
using FluentAssertions; using FluentAssertions;
using Xunit; 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.Commands.Users.DeleteUser;
using CleanArchitecture.Domain.Events.User; using CleanArchitecture.Domain.Events.User;
using CleanArchitecture.Domain.Notifications; 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.Database;
using CleanArchitecture.Infrastructure.Tests.Fixtures; using CleanArchitecture.Infrastructure.Tests.Fixtures;
using FluentAssertions; using FluentAssertions;
@ -37,7 +40,7 @@ public sealed class UnitOfWorkTests
dbContextMock dbContextMock
.Setup(x => x.SaveChangesAsync(CancellationToken.None)) .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); var unitOfWork = UnitOfWorkTestFixture.GetUnitOfWork(dbContextMock.Object, loggerMock.Object);
@ -59,8 +62,8 @@ public sealed class UnitOfWorkTests
var unitOfWork = UnitOfWorkTestFixture.GetUnitOfWork(dbContextMock.Object, loggerMock.Object); 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 public class BaseRepository<TEntity> : IRepository<TEntity> where TEntity : Entity
{ {
protected readonly DbContext _dbContext; private readonly DbContext _dbContext;
protected readonly DbSet<TEntity> _dbSet; protected readonly DbSet<TEntity> DbSet;
public BaseRepository(DbContext context) protected BaseRepository(DbContext context)
{ {
_dbContext = context; _dbContext = context;
_dbSet = _dbContext.Set<TEntity>(); DbSet = _dbContext.Set<TEntity>();
} }
public void Add(TEntity entity) public void Add(TEntity entity)
{ {
_dbSet.Add(entity); DbSet.Add(entity);
} }
public void AddRange(IEnumerable<TEntity> entities) public void AddRange(IEnumerable<TEntity> entities)
{ {
_dbSet.AddRange(entities); DbSet.AddRange(entities);
} }
public void Dispose() public void Dispose()
@ -37,17 +37,17 @@ public class BaseRepository<TEntity> : IRepository<TEntity> where TEntity : Enti
public virtual IQueryable<TEntity> GetAll() public virtual IQueryable<TEntity> GetAll()
{ {
return _dbSet; return DbSet;
} }
public virtual IQueryable<TEntity> GetAllNoTracking() public virtual IQueryable<TEntity> GetAllNoTracking()
{ {
return _dbSet.AsNoTracking(); return DbSet.AsNoTracking();
} }
public virtual async Task<TEntity?> GetByIdAsync(Guid id) public virtual async Task<TEntity?> GetByIdAsync(Guid id)
{ {
return await _dbSet.FindAsync(id); return await DbSet.FindAsync(id);
} }
public int SaveChanges() public int SaveChanges()
@ -65,24 +65,24 @@ public class BaseRepository<TEntity> : IRepository<TEntity> where TEntity : Enti
public virtual void Update(TEntity entity) public virtual void Update(TEntity entity)
{ {
_dbSet.Update(entity); DbSet.Update(entity);
} }
public Task<bool> ExistsAsync(Guid id) 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) public void Remove(TEntity entity, bool hardDelete = false)
{ {
if (hardDelete) if (hardDelete)
{ {
_dbSet.Remove(entity); DbSet.Remove(entity);
} }
else else
{ {
entity.Delete(); 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) 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; 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 TContext _context;
private readonly ILogger<UnitOfWork<TContext>> _logger; private readonly ILogger<UnitOfWork<TContext>> _logger;
@ -34,10 +34,11 @@ public class UnitOfWork<TContext> : IUnitOfWork where TContext : DbContext
public void Dispose() public void Dispose()
{ {
Dispose(true); Dispose(true);
// ReSharper disable once GCSuppressFinalizeForTypeWithoutDestructor
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
protected virtual void Dispose(bool disposing) private void Dispose(bool disposing)
{ {
if (disposing) if (disposing)
{ {

View File

@ -2,7 +2,6 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IsPackable>false</IsPackable> <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.Application.ViewModels.Users;
using CleanArchitecture.IntegrationTests.Extensions; using CleanArchitecture.IntegrationTests.Extensions;
using CleanArchitecture.IntegrationTests.Fixtures; using CleanArchitecture.IntegrationTests.Fixtures;
@ -8,7 +12,7 @@ using Xunit.Priority;
namespace CleanArchitecture.IntegrationTests.Controller; namespace CleanArchitecture.IntegrationTests.Controller;
[Collection("Integrationtests")] [Collection("IntegrationTests")]
[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)] [TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
public sealed class UserControllerTests : IClassFixture<UserTestFixture> public sealed class UserControllerTests : IClassFixture<UserTestFixture>
{ {
@ -122,7 +126,7 @@ public sealed class UserControllerTests : IClassFixture<UserTestFixture>
message?.Data.Should().NotBeNull(); message?.Data.Should().NotBeNull();
var content = message!.Data!; var content = message!.Data!.ToList();
content.Should().ContainSingle(); content.Should().ContainSingle();
content.First().Id.Should().Be(_fixture.CreatedUserId); content.First().Id.Should().Be(_fixture.CreatedUserId);
@ -142,7 +146,7 @@ public sealed class UserControllerTests : IClassFixture<UserTestFixture>
message?.Data.Should().NotBeEmpty(); message?.Data.Should().NotBeEmpty();
var content = message!.Data!; var content = message!.Data;
content.Should().Be(_fixture.CreatedUserId); 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.Infrastructure;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -17,7 +20,7 @@ public static class FunctionalTestsServiceCollectionExtensions
services.AddScoped(p => services.AddScoped(p =>
DbContextOptionsFactory<TContext>( DbContextOptionsFactory<TContext>(
p, p,
(sp, options) => options (_, options) => options
.ConfigureWarnings(b => b.Log(CoreEventId.ManyServiceProvidersCreatedWarning)) .ConfigureWarnings(b => b.Log(CoreEventId.ManyServiceProvidersCreatedWarning))
.UseLazyLoadingProxies() .UseLazyLoadingProxies()
.UseSqlite(connection))); .UseSqlite(connection)));
@ -35,7 +38,7 @@ public static class FunctionalTestsServiceCollectionExtensions
builder.UseApplicationServiceProvider(applicationServiceProvider); builder.UseApplicationServiceProvider(applicationServiceProvider);
optionsAction?.Invoke(applicationServiceProvider, builder); optionsAction.Invoke(applicationServiceProvider, builder);
return builder.Options; 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.Text.Json;
using System.Threading.Tasks;
using CleanArchitecture.Api.Models; using CleanArchitecture.Api.Models;
namespace CleanArchitecture.IntegrationTests.Extensions; 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, 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 CleanArchitecture.IntegrationTests.Infrastructure;
using Microsoft.Extensions.DependencyInjection; 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 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.Infrastructure.Extensions;
using CleanArchitecture.IntegrationTests.Extensions; using CleanArchitecture.IntegrationTests.Extensions;
using Microsoft.AspNetCore.Hosting; 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 EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.IntegrationTests", "CleanArchitecture.IntegrationTests\CleanArchitecture.IntegrationTests.csproj", "{39732BD4-909F-410C-8737-1F9FE3E269A7}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.IntegrationTests", "CleanArchitecture.IntegrationTests\CleanArchitecture.IntegrationTests.csproj", "{39732BD4-909F-410C-8737-1F9FE3E269A7}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{39732BD4-909F-410C-8737-1F9FE3E269A7}.Release|Any CPU.Build.0 = 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 EndGlobalSection
EndGlobal EndGlobal

View File

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