diff --git a/.gitignore b/.gitignore index 91953df..47d875a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ obj/ /packages/ riderModule.iml /_ReSharper.Caches/ -.idea \ No newline at end of file +.idea +.vs diff --git a/CleanArchitecture.Api/CleanArchitecture.Api.csproj b/CleanArchitecture.Api/CleanArchitecture.Api.csproj index 8589472..749f190 100644 --- a/CleanArchitecture.Api/CleanArchitecture.Api.csproj +++ b/CleanArchitecture.Api/CleanArchitecture.Api.csproj @@ -22,6 +22,7 @@ + diff --git a/CleanArchitecture.Api/Controllers/UserController.cs b/CleanArchitecture.Api/Controllers/UserController.cs index 859ee65..a1dc752 100644 --- a/CleanArchitecture.Api/Controllers/UserController.cs +++ b/CleanArchitecture.Api/Controllers/UserController.cs @@ -1,4 +1,6 @@ using System; +using System.Threading.Tasks; +using CleanArchitecture.Application.Interfaces; using CleanArchitecture.Domain.Notifications; using MediatR; using Microsoft.AspNetCore.Mvc; @@ -9,8 +11,13 @@ namespace CleanArchitecture.Api.Controllers; [Route("[controller]")] public class UserController : ApiController { - public UserController(NotificationHandler notifications) : base(notifications) + private readonly IUserService _userService; + + public UserController( + INotificationHandler notifications, + IUserService userService) : base(notifications) { + _userService = userService; } [HttpGet] @@ -20,9 +27,10 @@ public class UserController : ApiController } [HttpGet("{id}")] - public string GetUserByIdAsync([FromRoute] Guid id) + public async Task GetUserByIdAsync([FromRoute] Guid id) { - return "test"; + var user = await _userService.GetUserByUserIdAsync(id); + return Response(user); } [HttpPost] diff --git a/CleanArchitecture.Api/Program.cs b/CleanArchitecture.Api/Program.cs index df305b8..06f6b4e 100644 --- a/CleanArchitecture.Api/Program.cs +++ b/CleanArchitecture.Api/Program.cs @@ -1,9 +1,10 @@ +using CleanArchitecture.Application.Extensions; using CleanArchitecture.Infrastructure.Database; +using CleanArchitecture.Infrastructure.Extensions; using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; var builder = WebApplication.CreateBuilder(args); @@ -11,13 +12,19 @@ builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); +builder.Services.AddHttpContextAccessor(); + builder.Services.AddDbContext(options => { options.UseLazyLoadingProxies(); options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"), - b => b.MigrationsAssembly("netgo.centralhub.TenantService.Infrastructure")); + b => b.MigrationsAssembly("CleanArchitecture.Infrastructure")); }); +builder.Services.AddInfrastructure(); +builder.Services.AddQueryHandlers(); +builder.Services.AddServices(); + builder.Services.AddMediatR(cfg => { cfg.RegisterServicesFromAssemblies(typeof(Program).Assembly); @@ -25,12 +32,8 @@ builder.Services.AddMediatR(cfg => var app = builder.Build(); -// Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} +app.UseSwagger(); +app.UseSwaggerUI(); app.UseHttpsRedirection(); @@ -38,6 +41,14 @@ app.UseAuthorization(); app.MapControllers(); +using (IServiceScope scope = app.Services.CreateScope()) +{ + var services = scope.ServiceProvider; + ApplicationDbContext appDbContext = services.GetRequiredService(); + + appDbContext.EnsureMigrationsApplied(); +} + app.Run(); // Needed for integration tests webapplication factory diff --git a/CleanArchitecture.Api/Properties/launchSettings.json b/CleanArchitecture.Api/Properties/launchSettings.json index 928c63e..9894d69 100644 --- a/CleanArchitecture.Api/Properties/launchSettings.json +++ b/CleanArchitecture.Api/Properties/launchSettings.json @@ -9,20 +9,10 @@ } }, "profiles": { - "http": { + "CleanArchitecture.Api": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "http://localhost:5201", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "launchUrl": "swagger", "applicationUrl": "https://localhost:7260;http://localhost:5201", "environmentVariables": { diff --git a/CleanArchitecture.Api/appsettings.json b/CleanArchitecture.Api/appsettings.json index 10f68b8..26b0156 100644 --- a/CleanArchitecture.Api/appsettings.json +++ b/CleanArchitecture.Api/appsettings.json @@ -5,5 +5,8 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Server=localhost;Database=clean-architecture;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True" + } } diff --git a/CleanArchitecture.Application/CleanArchitecture.Application.csproj b/CleanArchitecture.Application/CleanArchitecture.Application.csproj index 705670a..347d094 100644 --- a/CleanArchitecture.Application/CleanArchitecture.Application.csproj +++ b/CleanArchitecture.Application/CleanArchitecture.Application.csproj @@ -6,12 +6,12 @@ - - - - - + + + + + diff --git a/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs b/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs index 4cd5d74..811bca4 100644 --- a/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs +++ b/CleanArchitecture.Application/Extensions/ServiceCollectionExtension.cs @@ -1,5 +1,8 @@ using CleanArchitecture.Application.Interfaces; +using CleanArchitecture.Application.Queries.Users.GetUserById; using CleanArchitecture.Application.Services; +using CleanArchitecture.Application.ViewModels; +using MediatR; using Microsoft.Extensions.DependencyInjection; namespace CleanArchitecture.Application.Extensions; @@ -15,7 +18,7 @@ public static class ServiceCollectionExtension public static IServiceCollection AddQueryHandlers(this IServiceCollection services) { - // services.AddScoped, GetUserByIdQueryHandler>(); + services.AddScoped, GetUserByIdQueryHandler>(); return services; } diff --git a/CleanArchitecture.Application/Interfaces/IUserService.cs b/CleanArchitecture.Application/Interfaces/IUserService.cs index 434842e..996e6d1 100644 --- a/CleanArchitecture.Application/Interfaces/IUserService.cs +++ b/CleanArchitecture.Application/Interfaces/IUserService.cs @@ -1,6 +1,10 @@ +using CleanArchitecture.Application.ViewModels; +using System.Threading.Tasks; +using System; + namespace CleanArchitecture.Application.Interfaces; public interface IUserService { - + public Task GetUserByUserIdAsync(Guid userId); } \ No newline at end of file diff --git a/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQuery.cs b/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQuery.cs new file mode 100644 index 0000000..0cf4a82 --- /dev/null +++ b/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQuery.cs @@ -0,0 +1,7 @@ +using System; +using CleanArchitecture.Application.ViewModels; +using MediatR; + +namespace CleanArchitecture.Application.Queries.Users.GetUserById; + +public sealed record GetUserByIdQuery(Guid UserId) : IRequest; diff --git a/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs b/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs new file mode 100644 index 0000000..3a96b7b --- /dev/null +++ b/CleanArchitecture.Application/Queries/Users/GetUserById/GetUserByIdQueryHandler.cs @@ -0,0 +1,44 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CleanArchitecture.Application.ViewModels; +using CleanArchitecture.Domain.Errors; +using CleanArchitecture.Domain.Interfaces; +using CleanArchitecture.Domain.Interfaces.Repositories; +using CleanArchitecture.Domain.Notifications; +using MediatR; + +namespace CleanArchitecture.Application.Queries.Users.GetUserById; + +public sealed class GetUserByIdQueryHandler : + IRequestHandler +{ + private readonly IUserRepository _userRepository; + private readonly IMediatorHandler _bus; + + public GetUserByIdQueryHandler(IUserRepository userRepository, IMediatorHandler bus) + { + _userRepository = userRepository; + _bus = bus; + } + + public async Task Handle(GetUserByIdQuery request, CancellationToken cancellationToken) + { + var user = _userRepository + .GetAllNoTracking() + .Where(x => x.Id == request.UserId) + .FirstOrDefault(); + + if (user == null) + { + await _bus.RaiseEventAsync( + new DomainNotification( + nameof(GetUserByIdQuery), + $"User with id {request.UserId} could not be found", + ErrorCodes.ObjectNotFound)); + return null; + } + + return UserViewModel.FromUser(user); + } +} diff --git a/CleanArchitecture.Application/Services/UserService.cs b/CleanArchitecture.Application/Services/UserService.cs index 5c3d82a..61b3687 100644 --- a/CleanArchitecture.Application/Services/UserService.cs +++ b/CleanArchitecture.Application/Services/UserService.cs @@ -1,7 +1,23 @@ +using System; +using System.Threading.Tasks; using CleanArchitecture.Application.Interfaces; +using CleanArchitecture.Application.Queries.Users.GetUserById; +using CleanArchitecture.Application.ViewModels; +using CleanArchitecture.Domain.Interfaces; namespace CleanArchitecture.Application.Services; public sealed class UserService : IUserService { + private readonly IMediatorHandler _bus; + + public UserService(IMediatorHandler bus) + { + _bus = bus; + } + + public async Task GetUserByUserIdAsync(Guid userId) + { + return await _bus.QueryAsync(new GetUserByIdQuery(userId)); + } } \ No newline at end of file diff --git a/CleanArchitecture.Application/viewmodels/UserViewModel.cs b/CleanArchitecture.Application/viewmodels/UserViewModel.cs new file mode 100644 index 0000000..f59a955 --- /dev/null +++ b/CleanArchitecture.Application/viewmodels/UserViewModel.cs @@ -0,0 +1,23 @@ +using System; +using CleanArchitecture.Domain.Entities; + +namespace CleanArchitecture.Application.ViewModels; + +public sealed class UserViewModel +{ + public Guid Id { get; set; } + public string Email { get; set; } = string.Empty; + public string GivenName { get; set; } = string.Empty; + public string Surname { get; set; } = string.Empty; + + public static UserViewModel FromUser(User user) + { + return new UserViewModel + { + Id = user.Id, + Email = user.Email, + GivenName = user.GivenName, + Surname = user.Surname + }; + } +} diff --git a/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj b/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj index 8dee848..9a365b9 100644 --- a/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj +++ b/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj @@ -12,6 +12,12 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/CleanArchitecture.Infrastructure/Extensions/DbContextExtension.cs b/CleanArchitecture.Infrastructure/Extensions/DbContextExtension.cs new file mode 100644 index 0000000..9192d48 --- /dev/null +++ b/CleanArchitecture.Infrastructure/Extensions/DbContextExtension.cs @@ -0,0 +1,21 @@ +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace CleanArchitecture.Infrastructure.Extensions; + +public static class DbContextExtension +{ + public static void EnsureMigrationsApplied(this DbContext context) + { + var applied = context.GetService().GetAppliedMigrations().Select(m => m.MigrationId); + + var total = context.GetService().Migrations.Select(m => m.Key); + + if (total.Except(applied).Any()) + { + context.Database.Migrate(); + } + } +} diff --git a/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs b/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..ff72bb8 --- /dev/null +++ b/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,25 @@ +using CleanArchitecture.Domain.Interfaces; +using CleanArchitecture.Domain.Interfaces.Repositories; +using CleanArchitecture.Domain.Notifications; +using CleanArchitecture.Infrastructure.Database; +using CleanArchitecture.Infrastructure.Repositories; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace CleanArchitecture.Infrastructure.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddInfrastructure(this IServiceCollection services) + { + // Core Infra + services.AddScoped>(); + services.AddScoped, DomainNotificationHandler>(); + services.AddScoped(); + + // Repositories + services.AddScoped(); + + return services; + } +} diff --git a/CleanArchitecture.Infrastructure/Migrations/20230306172301_InitialMigration.Designer.cs b/CleanArchitecture.Infrastructure/Migrations/20230306172301_InitialMigration.Designer.cs new file mode 100644 index 0000000..0188e90 --- /dev/null +++ b/CleanArchitecture.Infrastructure/Migrations/20230306172301_InitialMigration.Designer.cs @@ -0,0 +1,62 @@ +// +using System; +using CleanArchitecture.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CleanArchitecture.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230306172301_InitialMigration")] + partial class InitialMigration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.3") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CleanArchitecture.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("nvarchar(320)"); + + b.Property("GivenName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Surname") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CleanArchitecture.Infrastructure/Migrations/20230306172301_InitialMigration.cs b/CleanArchitecture.Infrastructure/Migrations/20230306172301_InitialMigration.cs new file mode 100644 index 0000000..03cd4ee --- /dev/null +++ b/CleanArchitecture.Infrastructure/Migrations/20230306172301_InitialMigration.cs @@ -0,0 +1,37 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CleanArchitecture.Infrastructure.Migrations +{ + /// + public partial class InitialMigration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Email = table.Column(type: "nvarchar(320)", maxLength: 320, nullable: false), + GivenName = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + Surname = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + Deleted = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..dcc40d6 --- /dev/null +++ b/CleanArchitecture.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,59 @@ +// +using System; +using CleanArchitecture.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CleanArchitecture.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.3") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CleanArchitecture.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Deleted") + .HasColumnType("bit"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("nvarchar(320)"); + + b.Property("GivenName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Surname") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +}