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
+ }
+ }
+}