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

Add initial infra

This commit is contained in:
alex289 2023-03-06 15:51:24 +01:00
parent e9f66ddf0b
commit d884b03336
No known key found for this signature in database
GPG Key ID: 573F77CD2D87F863
44 changed files with 1129 additions and 26 deletions

View File

@ -7,7 +7,23 @@
<ItemGroup>
<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" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CleanArchitecture.Domain\CleanArchitecture.Domain.csproj" />
<ProjectReference Include="..\CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj" />
</ItemGroup>
</Project>

View 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;
}
}

View 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";
}
}

View File

@ -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";
}
}

View 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; }
}

View 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; }
}

View File

@ -1,16 +1,28 @@
using CleanArchitecture.Infrastructure.Database;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
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();
// Configure the HTTP request pipeline.
@ -26,4 +38,7 @@ app.UseAuthorization();
app.MapControllers();
app.Run();
app.Run();
// Needed for integration tests webapplication factory
public partial class Program { }

View File

@ -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>

View File

@ -0,0 +1,9 @@
namespace CleanArchitecture.Application.Tests;
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}

View File

@ -0,0 +1 @@
global using Xunit;

View File

@ -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>

View File

@ -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;
}
}

View File

@ -0,0 +1,6 @@
namespace CleanArchitecture.Application.Interfaces;
public interface IUserService
{
}

View File

@ -0,0 +1,7 @@
using CleanArchitecture.Application.Interfaces;
namespace CleanArchitecture.Application.Services;
public sealed class UserService : IUserService
{
}

View File

@ -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>

View 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;
}
}

View File

@ -0,0 +1,11 @@
using Xunit;
namespace CleanArchitecture.Domain.Tests;
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}

View 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);
}
}
}

View 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>

View 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();
}

View 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; }
}

View 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 + "]";
}
}

View 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;
}
}

View 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";
}

View 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);
}

View File

@ -0,0 +1,9 @@
using System;
using System.Threading.Tasks;
namespace CleanArchitecture.Domain.Interfaces;
public interface IUnitOfWork : IDisposable
{
public Task<bool> CommitAsync();
}

View File

@ -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);
}

View File

@ -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);
}

View 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;
}
}

View File

@ -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();
}
}

View File

@ -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>

View File

@ -0,0 +1,9 @@
namespace CleanArchitecture.Infrastructure.Tests;
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}

View File

@ -0,0 +1 @@
global using Xunit;

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" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MediatR" Version="12.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.3" />
</ItemGroup>
</Project>

View File

@ -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);
}
}

View File

@ -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());
}
}

View 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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View 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();
}
}
}

View File

@ -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>

View File

@ -0,0 +1,9 @@
namespace CleanArchitecture.IntegrationTests;
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}

View File

@ -0,0 +1 @@
global using Xunit;

View File

@ -2,6 +2,20 @@
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}"
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
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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}.Release|Any CPU.ActiveCfg = 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
EndGlobal