mirror of
https://github.com/alex289/CleanArchitecture.git
synced 2025-08-29 14:55:34 +00:00
Add initial infra
This commit is contained in:
parent
e9f66ddf0b
commit
d884b03336
@ -7,7 +7,23 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.1" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.3" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.3">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="7.0.3" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.3" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.3">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj" />
|
||||||
|
<ProjectReference Include="..\CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
68
CleanArchitecture.Api/Controllers/ApiController.cs
Normal file
68
CleanArchitecture.Api/Controllers/ApiController.cs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using CleanArchitecture.Api.Models;
|
||||||
|
using CleanArchitecture.Domain.Errors;
|
||||||
|
using CleanArchitecture.Domain.Notifications;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Api.Controllers;
|
||||||
|
|
||||||
|
public class ApiController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly DomainNotificationHandler _notifications;
|
||||||
|
|
||||||
|
protected ApiController(INotificationHandler<DomainNotification> notifications)
|
||||||
|
{
|
||||||
|
_notifications = (DomainNotificationHandler)notifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected new IActionResult Response(object? resultData = null)
|
||||||
|
{
|
||||||
|
if (!_notifications.HasNotifications())
|
||||||
|
{
|
||||||
|
return Ok(
|
||||||
|
new ResponseMessage<object>
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Data = resultData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var message = new ResponseMessage<object>
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Errors = _notifications.GetNotifications().Select(n => n.Value),
|
||||||
|
DetailedErrors = _notifications.GetNotifications().Select(n => new DetailedError
|
||||||
|
{
|
||||||
|
Code = n.Code,
|
||||||
|
Data = n.Data
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
return new ObjectResult(message)
|
||||||
|
{
|
||||||
|
StatusCode = (int)GetErrorStatusCode()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected HttpStatusCode GetStatusCode()
|
||||||
|
{
|
||||||
|
if (!_notifications.GetNotifications().Any())
|
||||||
|
{
|
||||||
|
return HttpStatusCode.OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetErrorStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected HttpStatusCode GetErrorStatusCode()
|
||||||
|
{
|
||||||
|
if (_notifications.GetNotifications().Any(n => n.Code == ErrorCodes.ObjectNotFound))
|
||||||
|
{
|
||||||
|
return HttpStatusCode.NotFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
return HttpStatusCode.BadRequest;
|
||||||
|
}
|
||||||
|
}
|
45
CleanArchitecture.Api/Controllers/UserController.cs
Normal file
45
CleanArchitecture.Api/Controllers/UserController.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
using CleanArchitecture.Domain.Notifications;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Api.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("[controller]")]
|
||||||
|
public class UserController : ApiController
|
||||||
|
{
|
||||||
|
public UserController(NotificationHandler<DomainNotification> notifications) : base(notifications)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public string GetAllUsersAsync()
|
||||||
|
{
|
||||||
|
return "test";
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
public string GetUserByIdAsync([FromRoute] Guid id)
|
||||||
|
{
|
||||||
|
return "test";
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public string CreateUserAsync()
|
||||||
|
{
|
||||||
|
return "test";
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
public string DeleteUserAsync([FromRoute] Guid id)
|
||||||
|
{
|
||||||
|
return "test";
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut]
|
||||||
|
public string UpdateUserAsync()
|
||||||
|
{
|
||||||
|
return "test";
|
||||||
|
}
|
||||||
|
}
|
@ -1,22 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace CleanArchitecture.Api.Controllers;
|
|
||||||
|
|
||||||
[ApiController]
|
|
||||||
[Route("[controller]")]
|
|
||||||
public class WeatherForecastController : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly ILogger<WeatherForecastController> _logger;
|
|
||||||
|
|
||||||
public WeatherForecastController(ILogger<WeatherForecastController> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet(Name = "GetWeatherForecast")]
|
|
||||||
public string Get()
|
|
||||||
{
|
|
||||||
return "test";
|
|
||||||
}
|
|
||||||
}
|
|
12
CleanArchitecture.Api/Models/DetailedError.cs
Normal file
12
CleanArchitecture.Api/Models/DetailedError.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Api.Models;
|
||||||
|
|
||||||
|
public sealed class DetailedError
|
||||||
|
{
|
||||||
|
[JsonPropertyName("code")]
|
||||||
|
public string Code { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public object? Data { get; init; }
|
||||||
|
}
|
20
CleanArchitecture.Api/Models/ResponseMessage.cs
Normal file
20
CleanArchitecture.Api/Models/ResponseMessage.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Api.Models;
|
||||||
|
|
||||||
|
public sealed class ResponseMessage<T>
|
||||||
|
{
|
||||||
|
[JsonPropertyName("success")]
|
||||||
|
public bool Success { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("errors")]
|
||||||
|
public IEnumerable<string>? Errors { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("detailedErrors")]
|
||||||
|
public IEnumerable<DetailedError> DetailedErrors { get; init; } = Enumerable.Empty<DetailedError>();
|
||||||
|
|
||||||
|
[JsonPropertyName("data")]
|
||||||
|
public T? Data { get; init; }
|
||||||
|
}
|
@ -1,16 +1,28 @@
|
|||||||
|
using CleanArchitecture.Infrastructure.Database;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Add services to the container.
|
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen();
|
||||||
|
|
||||||
|
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
||||||
|
{
|
||||||
|
options.UseLazyLoadingProxies();
|
||||||
|
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"),
|
||||||
|
b => b.MigrationsAssembly("netgo.centralhub.TenantService.Infrastructure"));
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddMediatR(cfg =>
|
||||||
|
{
|
||||||
|
cfg.RegisterServicesFromAssemblies(typeof(Program).Assembly);
|
||||||
|
});
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
@ -26,4 +38,7 @@ app.UseAuthorization();
|
|||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
|
// Needed for integration tests webapplication factory
|
||||||
|
public partial class Program { }
|
@ -0,0 +1,24 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2"/>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
</Project>
|
9
CleanArchitecture.Application.Tests/UnitTest1.cs
Normal file
9
CleanArchitecture.Application.Tests/UnitTest1.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace CleanArchitecture.Application.Tests;
|
||||||
|
|
||||||
|
public class UnitTest1
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Test1()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
1
CleanArchitecture.Application.Tests/Usings.cs
Normal file
1
CleanArchitecture.Application.Tests/Usings.cs
Normal file
@ -0,0 +1 @@
|
|||||||
|
global using Xunit;
|
@ -0,0 +1,17 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Queries" />
|
||||||
|
<Folder Include="ViewModels" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
@ -0,0 +1,22 @@
|
|||||||
|
using CleanArchitecture.Application.Interfaces;
|
||||||
|
using CleanArchitecture.Application.Services;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Application.Extensions;
|
||||||
|
|
||||||
|
public static class ServiceCollectionExtension
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddServices(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<IUserService, UserService>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddQueryHandlers(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
// services.AddScoped<IQueryHandler<GetUserByIdQuery, User>, GetUserByIdQueryHandler>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
6
CleanArchitecture.Application/Interfaces/IUserService.cs
Normal file
6
CleanArchitecture.Application/Interfaces/IUserService.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace CleanArchitecture.Application.Interfaces;
|
||||||
|
|
||||||
|
public interface IUserService
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
7
CleanArchitecture.Application/Services/UserService.cs
Normal file
7
CleanArchitecture.Application/Services/UserService.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
using CleanArchitecture.Application.Interfaces;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Application.Services;
|
||||||
|
|
||||||
|
public sealed class UserService : IUserService
|
||||||
|
{
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<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="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" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
90
CleanArchitecture.Domain.Tests/CommandHandlerFixtureBase.cs
Normal file
90
CleanArchitecture.Domain.Tests/CommandHandlerFixtureBase.cs
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
using System.Linq.Expressions;
|
||||||
|
using CleanArchitecture.Domain.Interfaces;
|
||||||
|
using CleanArchitecture.Domain.Notifications;
|
||||||
|
using Moq;
|
||||||
|
|
||||||
|
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 CommandHandlerFixtureBase()
|
||||||
|
{
|
||||||
|
Bus = new Mock<IMediatorHandler>();
|
||||||
|
UnitOfWork = new Mock<IUnitOfWork>();
|
||||||
|
NotificationHandler = new Mock<DomainNotificationHandler>();
|
||||||
|
|
||||||
|
UnitOfWork.Setup(unit => unit.CommitAsync()).ReturnsAsync(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandHandlerFixtureBase VerifyExistingNotification(string errorCode, string message)
|
||||||
|
{
|
||||||
|
Bus.Verify(
|
||||||
|
bus => bus.RaiseEventAsync(
|
||||||
|
It.Is<DomainNotification>(not => not.Code == errorCode && not.Value == message)),
|
||||||
|
Times.Once);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandHandlerFixtureBase VerifyAnyDomainNotification()
|
||||||
|
{
|
||||||
|
Bus.Verify(
|
||||||
|
bus => bus.RaiseEventAsync(It.IsAny<DomainNotification>()),
|
||||||
|
Times.Once);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandHandlerFixtureBase VerifyNoDomainNotification()
|
||||||
|
{
|
||||||
|
Bus.Verify(
|
||||||
|
bus => bus.RaiseEventAsync(It.IsAny<DomainNotification>()),
|
||||||
|
Times.Never);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandHandlerFixtureBase VerifyNoRaisedEvent<TEvent>()
|
||||||
|
where TEvent : DomainEvent
|
||||||
|
{
|
||||||
|
Bus.Verify(
|
||||||
|
bus => bus.RaiseEventAsync(It.IsAny<TEvent>()),
|
||||||
|
Times.Never);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandHandlerFixtureBase VerifyNoRaisedEvent<TEvent>(Expression<Func<TEvent, bool>> checkFunction)
|
||||||
|
where TEvent : DomainEvent
|
||||||
|
{
|
||||||
|
Bus.Verify(bus => bus.RaiseEventAsync(It.Is(checkFunction)), Times.Never);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandHandlerFixtureBase VerifyNoCommit()
|
||||||
|
{
|
||||||
|
UnitOfWork.Verify(unit => unit.CommitAsync(), Times.Never);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandHandlerFixtureBase VerifyCommit()
|
||||||
|
{
|
||||||
|
UnitOfWork.Verify(unit => unit.CommitAsync(), Times.Once);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandHandlerFixtureBase VerifyRaisedEvent<TEvent>(Expression<Func<TEvent, bool>> checkFunction)
|
||||||
|
where TEvent : DomainEvent
|
||||||
|
{
|
||||||
|
Bus.Verify(bus => bus.RaiseEventAsync(It.Is(checkFunction)), Times.Once);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
11
CleanArchitecture.Domain.Tests/UnitTest1.cs
Normal file
11
CleanArchitecture.Domain.Tests/UnitTest1.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Domain.Tests;
|
||||||
|
|
||||||
|
public class UnitTest1
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Test1()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
71
CleanArchitecture.Domain.Tests/ValidationTestBase.cs
Normal file
71
CleanArchitecture.Domain.Tests/ValidationTestBase.cs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
using CleanArchitecture.Domain.Commands;
|
||||||
|
using FluentAssertions;
|
||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Domain.Tests;
|
||||||
|
|
||||||
|
public class ValidationTestBase<TCommand, TValidation>
|
||||||
|
where TCommand : CommandBase
|
||||||
|
where TValidation: AbstractValidator<TCommand>
|
||||||
|
{
|
||||||
|
protected readonly TValidation _validation;
|
||||||
|
|
||||||
|
protected ValidationTestBase(TValidation validation)
|
||||||
|
{
|
||||||
|
_validation = validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ShouldBeValid(TCommand command)
|
||||||
|
{
|
||||||
|
var result = _validation.Validate(command);
|
||||||
|
|
||||||
|
result.IsValid.Should().BeTrue();
|
||||||
|
result.Errors.Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ShouldHaveSingleError(
|
||||||
|
TCommand command,
|
||||||
|
string expectedCode)
|
||||||
|
{
|
||||||
|
var result = _validation.Validate(command);
|
||||||
|
|
||||||
|
result.IsValid.Should().BeFalse();
|
||||||
|
|
||||||
|
result.Errors.Count.Should().Be(1);
|
||||||
|
|
||||||
|
result.Errors.First().ErrorCode.Should().Be(expectedCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ShouldHaveSingleError(
|
||||||
|
TCommand command,
|
||||||
|
string expectedCode,
|
||||||
|
string expectedMessage)
|
||||||
|
{
|
||||||
|
var result = _validation.Validate(command);
|
||||||
|
|
||||||
|
result.IsValid.Should().BeFalse();
|
||||||
|
|
||||||
|
result.Errors.Count.Should().Be(1);
|
||||||
|
|
||||||
|
result.Errors.First().ErrorCode.Should().Be(expectedCode);
|
||||||
|
result.Errors.First().ErrorMessage.Should().Be(expectedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ShouldHaveExpectedErrors(
|
||||||
|
TCommand command,
|
||||||
|
params KeyValuePair<string, string>[] expectedErrors)
|
||||||
|
{
|
||||||
|
var result = _validation.Validate(command);
|
||||||
|
|
||||||
|
result.IsValid.Should().BeFalse();
|
||||||
|
result.Errors.Count.Should().Be(expectedErrors.Length);
|
||||||
|
|
||||||
|
foreach (var error in expectedErrors)
|
||||||
|
{
|
||||||
|
result.Errors
|
||||||
|
.Count(validation => validation.ErrorCode == error.Key && validation.ErrorMessage == error.Value)
|
||||||
|
.Should()
|
||||||
|
.Be(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
CleanArchitecture.Domain/CleanArchitecture.Domain.csproj
Normal file
13
CleanArchitecture.Domain/CleanArchitecture.Domain.csproj
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="FluentValidation" Version="11.5.1" />
|
||||||
|
<PackageReference Include="MediatR" Version="12.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
22
CleanArchitecture.Domain/Commands/CommandBase.cs
Normal file
22
CleanArchitecture.Domain/Commands/CommandBase.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
using System;
|
||||||
|
using FluentValidation.Results;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Domain.Commands;
|
||||||
|
|
||||||
|
public abstract class CommandBase : IRequest
|
||||||
|
{
|
||||||
|
public Guid AggregateId { get; }
|
||||||
|
public string MessageType { get; }
|
||||||
|
public DateTime Timestamp { get; }
|
||||||
|
public ValidationResult? ValidationResult { get; protected set; }
|
||||||
|
|
||||||
|
protected CommandBase(Guid aggregateId)
|
||||||
|
{
|
||||||
|
MessageType = GetType().Name;
|
||||||
|
Timestamp = DateTime.Now;
|
||||||
|
AggregateId = aggregateId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract bool IsValid();
|
||||||
|
}
|
14
CleanArchitecture.Domain/DomainEvent.cs
Normal file
14
CleanArchitecture.Domain/DomainEvent.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Domain;
|
||||||
|
|
||||||
|
public abstract class DomainEvent : INotification
|
||||||
|
{
|
||||||
|
protected DomainEvent(Guid aggregateId)
|
||||||
|
{
|
||||||
|
Timestamp = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DateTime Timestamp { get; }
|
||||||
|
}
|
61
CleanArchitecture.Domain/Entities/Entity.cs
Normal file
61
CleanArchitecture.Domain/Entities/Entity.cs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Domain.Entities;
|
||||||
|
|
||||||
|
public abstract class Entity
|
||||||
|
{
|
||||||
|
protected Entity(Guid id)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Guid Id { get; private set; }
|
||||||
|
public bool Deleted { get; private set; }
|
||||||
|
|
||||||
|
public void SetId(Guid id)
|
||||||
|
{
|
||||||
|
if (id == Guid.Empty)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"{nameof(id)} may not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
Id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Delete()
|
||||||
|
{
|
||||||
|
Deleted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Undelete()
|
||||||
|
{
|
||||||
|
Deleted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
{
|
||||||
|
var compareTo = obj as Entity;
|
||||||
|
|
||||||
|
if (ReferenceEquals(this, compareTo))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (compareTo is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Id == compareTo.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return GetType().GetHashCode() * 907 + Id.GetHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return GetType().Name + " [Id=" + Id + "]";
|
||||||
|
}
|
||||||
|
}
|
75
CleanArchitecture.Domain/Entities/User.cs
Normal file
75
CleanArchitecture.Domain/Entities/User.cs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Domain.Entities;
|
||||||
|
|
||||||
|
public class User : Entity
|
||||||
|
{
|
||||||
|
public string Email { get; private set; }
|
||||||
|
public string GivenName { get; private set; }
|
||||||
|
public string Surname { get; private set; }
|
||||||
|
|
||||||
|
public string FullName => $"{Surname}, {GivenName}";
|
||||||
|
|
||||||
|
public User(
|
||||||
|
Guid id,
|
||||||
|
string email,
|
||||||
|
string givenName,
|
||||||
|
string surname) : base(id)
|
||||||
|
{
|
||||||
|
Email = email;
|
||||||
|
GivenName = givenName;
|
||||||
|
Surname = surname;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MemberNotNull(nameof(Email))]
|
||||||
|
public void SetEmail(string email)
|
||||||
|
{
|
||||||
|
if (email == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(email));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (email.Length > 320)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
"Email may not be longer than 320 characters.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Email = email;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MemberNotNull(nameof(GivenName))]
|
||||||
|
public void SetGivenName(string givenName)
|
||||||
|
{
|
||||||
|
if (givenName == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(givenName));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (givenName.Length > 100)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
"Given name may not be longer than 100 characters");
|
||||||
|
}
|
||||||
|
|
||||||
|
GivenName = givenName;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MemberNotNull(nameof(Surname))]
|
||||||
|
public void SetSurname(string surname)
|
||||||
|
{
|
||||||
|
if (surname == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(surname));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (surname.Length > 100)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
"Surname may not be longer than 100 characters");
|
||||||
|
}
|
||||||
|
|
||||||
|
Surname = surname;
|
||||||
|
}
|
||||||
|
}
|
7
CleanArchitecture.Domain/Errors/ErrorCodes.cs
Normal file
7
CleanArchitecture.Domain/Errors/ErrorCodes.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace CleanArchitecture.Domain.Errors;
|
||||||
|
|
||||||
|
public static class ErrorCodes
|
||||||
|
{
|
||||||
|
public const string CommitFailed = "COMMIT_FAILED";
|
||||||
|
public const string ObjectNotFound = "OBJECT_NOT_FOUND";
|
||||||
|
}
|
14
CleanArchitecture.Domain/Interfaces/IMediatorHandler.cs
Normal file
14
CleanArchitecture.Domain/Interfaces/IMediatorHandler.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using CleanArchitecture.Domain.Commands;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Domain.Interfaces;
|
||||||
|
|
||||||
|
public interface IMediatorHandler
|
||||||
|
{
|
||||||
|
Task RaiseEventAsync<T>(T @event) where T : DomainEvent;
|
||||||
|
|
||||||
|
Task SendCommandAsync<T>(T command) where T : CommandBase;
|
||||||
|
|
||||||
|
Task<TResponse> QueryAsync<TResponse>(IRequest<TResponse> query);
|
||||||
|
}
|
9
CleanArchitecture.Domain/Interfaces/IUnitOfWork.cs
Normal file
9
CleanArchitecture.Domain/Interfaces/IUnitOfWork.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Domain.Interfaces;
|
||||||
|
|
||||||
|
public interface IUnitOfWork : IDisposable
|
||||||
|
{
|
||||||
|
public Task<bool> CommitAsync();
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CleanArchitecture.Domain.Entities;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Domain.Interfaces.Repositories;
|
||||||
|
|
||||||
|
public interface IRepository<TEntity> : IDisposable where TEntity : Entity
|
||||||
|
{
|
||||||
|
void Add(TEntity entity);
|
||||||
|
|
||||||
|
void AddRange(IEnumerable<TEntity> entities);
|
||||||
|
|
||||||
|
IQueryable<TEntity> GetAll();
|
||||||
|
|
||||||
|
IQueryable<TEntity> GetAllNoTracking();
|
||||||
|
|
||||||
|
Task<TEntity?> GetByIdAsync(Guid id);
|
||||||
|
|
||||||
|
void Update(TEntity entity);
|
||||||
|
|
||||||
|
Task<bool> ExistsAsync(Guid id);
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using CleanArchitecture.Domain.Entities;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Domain.Interfaces.Repositories;
|
||||||
|
|
||||||
|
public interface IUserRepository : IRepository<User>
|
||||||
|
{
|
||||||
|
Task<User?> GetByEmailAsync(string email);
|
||||||
|
}
|
26
CleanArchitecture.Domain/Notifications/DomainNotification.cs
Normal file
26
CleanArchitecture.Domain/Notifications/DomainNotification.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Domain.Notifications;
|
||||||
|
|
||||||
|
public sealed class DomainNotification : DomainEvent
|
||||||
|
{
|
||||||
|
public string Key { get; private set; }
|
||||||
|
public string Value { get; private set; }
|
||||||
|
public string Code { get; private set; }
|
||||||
|
public object? Data { get; set; }
|
||||||
|
|
||||||
|
public DomainNotification(
|
||||||
|
string key,
|
||||||
|
string value,
|
||||||
|
string code,
|
||||||
|
object? data = null,
|
||||||
|
Guid? aggregateId = null)
|
||||||
|
: base(aggregateId ?? Guid.Empty)
|
||||||
|
{
|
||||||
|
Key = key ?? throw new ArgumentNullException(nameof(key));
|
||||||
|
Value = value ?? throw new ArgumentNullException(nameof(value));
|
||||||
|
Code = code ?? throw new ArgumentNullException(nameof(code));
|
||||||
|
|
||||||
|
Data = data;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Domain.Notifications;
|
||||||
|
|
||||||
|
public class DomainNotificationHandler : INotificationHandler<DomainNotification>
|
||||||
|
{
|
||||||
|
private readonly List<DomainNotification> _notifications;
|
||||||
|
|
||||||
|
public DomainNotificationHandler()
|
||||||
|
{
|
||||||
|
_notifications = new List<DomainNotification>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual List<DomainNotification> GetNotifications()
|
||||||
|
{
|
||||||
|
return _notifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Handle(DomainNotification notification, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_notifications.Add(notification);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual bool HasNotifications()
|
||||||
|
{
|
||||||
|
return GetNotifications().Any();
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Clear()
|
||||||
|
{
|
||||||
|
_notifications.Clear();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2"/>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
</Project>
|
9
CleanArchitecture.Infrastructure.Tests/UnitTest1.cs
Normal file
9
CleanArchitecture.Infrastructure.Tests/UnitTest1.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace CleanArchitecture.Infrastructure.Tests;
|
||||||
|
|
||||||
|
public class UnitTest1
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Test1()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
1
CleanArchitecture.Infrastructure.Tests/Usings.cs
Normal file
1
CleanArchitecture.Infrastructure.Tests/Usings.cs
Normal file
@ -0,0 +1 @@
|
|||||||
|
global using Xunit;
|
@ -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" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="MediatR" Version="12.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
@ -0,0 +1,26 @@
|
|||||||
|
using CleanArchitecture.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Infrastructure.Configurations;
|
||||||
|
|
||||||
|
public sealed class UserConfiguration : IEntityTypeConfiguration<User>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<User> builder)
|
||||||
|
{
|
||||||
|
builder
|
||||||
|
.Property(user => user.Email)
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(320);
|
||||||
|
|
||||||
|
builder
|
||||||
|
.Property(user => user.GivenName)
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100);
|
||||||
|
|
||||||
|
builder
|
||||||
|
.Property(user => user.Surname)
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
using CleanArchitecture.Domain.Entities;
|
||||||
|
using CleanArchitecture.Infrastructure.Configurations;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Infrastructure.Database;
|
||||||
|
|
||||||
|
public sealed class ApplicationDbContext : DbContext
|
||||||
|
{
|
||||||
|
public DbSet<User> Users { get; set; } = null!;
|
||||||
|
|
||||||
|
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
|
{
|
||||||
|
builder.ApplyConfiguration(new UserConfiguration());
|
||||||
|
}
|
||||||
|
}
|
34
CleanArchitecture.Infrastructure/InMemoryBus.cs
Normal file
34
CleanArchitecture.Infrastructure/InMemoryBus.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using CleanArchitecture.Domain;
|
||||||
|
using CleanArchitecture.Domain.Commands;
|
||||||
|
using CleanArchitecture.Domain.Interfaces;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Infrastructure;
|
||||||
|
|
||||||
|
public sealed class InMemoryBus : IMediatorHandler
|
||||||
|
{
|
||||||
|
private readonly IMediator _mediator;
|
||||||
|
|
||||||
|
public InMemoryBus(IMediator mediator)
|
||||||
|
{
|
||||||
|
_mediator = mediator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<TResponse> QueryAsync<TResponse>(IRequest<TResponse> query)
|
||||||
|
{
|
||||||
|
return _mediator.Send(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RaiseEventAsync<T>(T @event) where T : DomainEvent
|
||||||
|
{
|
||||||
|
// await _domainEventStore.SaveAsync(@event);
|
||||||
|
|
||||||
|
await _mediator.Publish(@event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SendCommandAsync<T>(T command) where T : CommandBase
|
||||||
|
{
|
||||||
|
return _mediator.Send(command);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CleanArchitecture.Domain.Entities;
|
||||||
|
using CleanArchitecture.Domain.Interfaces;
|
||||||
|
using CleanArchitecture.Domain.Interfaces.Repositories;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Infrastructure.Repositories;
|
||||||
|
|
||||||
|
public class BaseRepository<TEntity> : IRepository<TEntity> where TEntity : Entity
|
||||||
|
{
|
||||||
|
protected readonly DbContext _dbContext;
|
||||||
|
protected readonly DbSet<TEntity> _dbSet;
|
||||||
|
|
||||||
|
public BaseRepository(DbContext context)
|
||||||
|
{
|
||||||
|
_dbContext = context;
|
||||||
|
_dbSet = _dbContext.Set<TEntity>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(TEntity entity)
|
||||||
|
{
|
||||||
|
_dbSet.Add(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddRange(IEnumerable<TEntity> entities)
|
||||||
|
{
|
||||||
|
_dbSet.AddRange(entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual IQueryable<TEntity> GetAll()
|
||||||
|
{
|
||||||
|
return _dbSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual IQueryable<TEntity> GetAllNoTracking()
|
||||||
|
{
|
||||||
|
return _dbSet.AsNoTracking();
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<TEntity?> GetByIdAsync(Guid id)
|
||||||
|
{
|
||||||
|
return await _dbSet.FindAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int SaveChanges()
|
||||||
|
{
|
||||||
|
return _dbContext.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
_dbContext.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Update(TEntity entity)
|
||||||
|
{
|
||||||
|
_dbSet.Update(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> ExistsAsync(Guid id)
|
||||||
|
{
|
||||||
|
return _dbSet.AnyAsync(entity => entity.Id == id);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using CleanArchitecture.Domain.Entities;
|
||||||
|
using CleanArchitecture.Domain.Interfaces.Repositories;
|
||||||
|
using CleanArchitecture.Infrastructure.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Infrastructure.Repositories;
|
||||||
|
|
||||||
|
public sealed class UserRepository : BaseRepository<User>, IUserRepository
|
||||||
|
{
|
||||||
|
public UserRepository(ApplicationDbContext context) : base(context)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<User?> GetByEmailAsync(string email)
|
||||||
|
{
|
||||||
|
return await _dbSet.SingleOrDefaultAsync(user => user.Email == email);
|
||||||
|
}
|
||||||
|
}
|
47
CleanArchitecture.Infrastructure/UnitOfWork.cs
Normal file
47
CleanArchitecture.Infrastructure/UnitOfWork.cs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CleanArchitecture.Domain.Interfaces;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace CleanArchitecture.Infrastructure;
|
||||||
|
|
||||||
|
public class UnitOfWork<TContext> : IUnitOfWork where TContext : DbContext
|
||||||
|
{
|
||||||
|
private readonly TContext _context;
|
||||||
|
private readonly ILogger<UnitOfWork<TContext>> _logger;
|
||||||
|
|
||||||
|
public UnitOfWork(TContext context, ILogger<UnitOfWork<TContext>> logger)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CommitAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (DbUpdateException dbUpdateException)
|
||||||
|
{
|
||||||
|
_logger.LogError(dbUpdateException, "An error occured during commiting changes");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
_context.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2"/>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
</Project>
|
9
CleanArchitecture.IntegrationTests/UnitTest1.cs
Normal file
9
CleanArchitecture.IntegrationTests/UnitTest1.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace CleanArchitecture.IntegrationTests;
|
||||||
|
|
||||||
|
public class UnitTest1
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Test1()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
1
CleanArchitecture.IntegrationTests/Usings.cs
Normal file
1
CleanArchitecture.IntegrationTests/Usings.cs
Normal file
@ -0,0 +1 @@
|
|||||||
|
global using Xunit;
|
@ -2,6 +2,20 @@
|
|||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Api", "CleanArchitecture.Api\CleanArchitecture.Api.csproj", "{CD720672-0ED9-4FDD-AD69-A416CB394318}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Api", "CleanArchitecture.Api\CleanArchitecture.Api.csproj", "{CD720672-0ED9-4FDD-AD69-A416CB394318}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Application", "CleanArchitecture.Application\CleanArchitecture.Application.csproj", "{859B50AF-9C8D-4489-B64A-EEBDF756A012}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Domain", "CleanArchitecture.Domain\CleanArchitecture.Domain.csproj", "{12C5BEEF-9BFD-450A-8627-6205702CA32B}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Infrastructure", "CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj", "{B6D046D8-D84A-4B7E-B05B-310B85EC8F1C}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Application.Tests", "CleanArchitecture.Application.Tests\CleanArchitecture.Application.Tests.csproj", "{6794B922-2AFD-4187-944D-7984B9973259}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Domain.Tests", "CleanArchitecture.Domain.Tests\CleanArchitecture.Domain.Tests.csproj", "{E1F25916-EBBE-4CBD-99A2-1EB2F604D55C}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.Infrastructure.Tests", "CleanArchitecture.Infrastructure.Tests\CleanArchitecture.Infrastructure.Tests.csproj", "{EC8122EB-C5E0-452F-B1CE-DA47DAEBC8F2}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CleanArchitecture.IntegrationTests", "CleanArchitecture.IntegrationTests\CleanArchitecture.IntegrationTests.csproj", "{39732BD4-909F-410C-8737-1F9FE3E269A7}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -12,5 +26,33 @@ Global
|
|||||||
{CD720672-0ED9-4FDD-AD69-A416CB394318}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{CD720672-0ED9-4FDD-AD69-A416CB394318}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{CD720672-0ED9-4FDD-AD69-A416CB394318}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{CD720672-0ED9-4FDD-AD69-A416CB394318}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{CD720672-0ED9-4FDD-AD69-A416CB394318}.Release|Any CPU.Build.0 = Release|Any CPU
|
{CD720672-0ED9-4FDD-AD69-A416CB394318}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{859B50AF-9C8D-4489-B64A-EEBDF756A012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{859B50AF-9C8D-4489-B64A-EEBDF756A012}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{859B50AF-9C8D-4489-B64A-EEBDF756A012}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{859B50AF-9C8D-4489-B64A-EEBDF756A012}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{12C5BEEF-9BFD-450A-8627-6205702CA32B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{12C5BEEF-9BFD-450A-8627-6205702CA32B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{12C5BEEF-9BFD-450A-8627-6205702CA32B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{12C5BEEF-9BFD-450A-8627-6205702CA32B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B6D046D8-D84A-4B7E-B05B-310B85EC8F1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B6D046D8-D84A-4B7E-B05B-310B85EC8F1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B6D046D8-D84A-4B7E-B05B-310B85EC8F1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B6D046D8-D84A-4B7E-B05B-310B85EC8F1C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{6794B922-2AFD-4187-944D-7984B9973259}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{6794B922-2AFD-4187-944D-7984B9973259}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{6794B922-2AFD-4187-944D-7984B9973259}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{6794B922-2AFD-4187-944D-7984B9973259}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{E1F25916-EBBE-4CBD-99A2-1EB2F604D55C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{E1F25916-EBBE-4CBD-99A2-1EB2F604D55C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{E1F25916-EBBE-4CBD-99A2-1EB2F604D55C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{E1F25916-EBBE-4CBD-99A2-1EB2F604D55C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{EC8122EB-C5E0-452F-B1CE-DA47DAEBC8F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{EC8122EB-C5E0-452F-B1CE-DA47DAEBC8F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{EC8122EB-C5E0-452F-B1CE-DA47DAEBC8F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{EC8122EB-C5E0-452F-B1CE-DA47DAEBC8F2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{39732BD4-909F-410C-8737-1F9FE3E269A7}.Debug|Any CPU.ActiveCfg = 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.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
Loading…
Reference in New Issue
Block a user