Compare commits
6 Commits
8d3783cfec
...
7a78a48d03
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a78a48d03 | ||
|
|
0b3249cb46 | ||
|
|
17fdb6ed51 | ||
|
|
166acea8b1 | ||
|
|
6c2b1884d2 | ||
|
|
3653def773 |
@@ -1,10 +1,8 @@
|
||||
using DbFirst.API.Middleware;
|
||||
using DbFirst.Application;
|
||||
using DbFirst.Application.Catalogs;
|
||||
using DbFirst.Domain.Repositories;
|
||||
using DbFirst.Application.Repositories;
|
||||
using DbFirst.Infrastructure;
|
||||
using DbFirst.Infrastructure.Repositories;
|
||||
using MediatR;
|
||||
using DbFirst.API.Middleware;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
|
||||
@@ -8,6 +8,18 @@
|
||||
"http://localhost:5101"
|
||||
]
|
||||
},
|
||||
"TableConfigurations": {
|
||||
"VwmyCatalog": {
|
||||
"ViewName": "VWMY_CATALOG",
|
||||
"GuidColumnName": "GUID",
|
||||
"CatTitleColumnName": "CAT_TITLE",
|
||||
"CatStringColumnName": "CAT_STRING",
|
||||
"AddedWhoColumnName": "ADDED_WHO",
|
||||
"AddedWhenColumnName": "ADDED_WHEN",
|
||||
"ChangedWhoColumnName": "CHANGED_WHO",
|
||||
"ChangedWhenColumnName": "CHANGED_WHEN"
|
||||
}
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
using AutoMapper;
|
||||
using DbFirst.Domain.Repositories;
|
||||
using DbFirst.Domain.Entities;
|
||||
using DbFirst.Domain;
|
||||
|
||||
namespace DbFirst.Application.Catalogs;
|
||||
|
||||
//TODO: create generic service to reduce code duplication
|
||||
|
||||
/* Copilot's Response:
|
||||
A generic CRUD base service adds little value in your case:
|
||||
|
||||
Pros:
|
||||
• Less boilerplate for simple entities without special logic.
|
||||
• Uniform CRUD signatures.
|
||||
|
||||
Cons/Practical here:
|
||||
• Domain logic differs per entity(unique title check, setting audit fields, forbidding title changes, stored procs with output GUID).
|
||||
• Generic services tend to be diluted by virtual methods/hooks for special cases—ending up with per-entity overrides and little real gain.
|
||||
• With stored procedures and output parameters, the pattern doesn’t fit cleanly because operations aren’t symmetric (separate procs for insert/update/delete).
|
||||
|
||||
Conclusion: For this solution a generic service would be more overhead than benefit. If you later have multiple very similar entities without special logic,
|
||||
you could consider a lightweight generic interface/base; for now, the specialized service implementation is cleaner. */
|
||||
|
||||
/* Hakan's Response:
|
||||
* No, it absolutely makes sense to create a generic service using Options pattern. So, you can easily inject your SQL queries or stored procedure names via configuration.
|
||||
* see: https://docs.microsoft.com/en-us/dotnet/core/extensions/options
|
||||
*/
|
||||
|
||||
//TODO: implement CQRS pattern with MediatR
|
||||
|
||||
/* Hakan's response
|
||||
* Here is the main part. We dont even need a service layer if we implement CQRS with MediatR at least for CRUD operations.
|
||||
*/
|
||||
|
||||
public class CatalogService : ICatalogService
|
||||
{
|
||||
private readonly ICatalogRepository _repository;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public CatalogService(ICatalogRepository repository, IMapper mapper)
|
||||
{
|
||||
_repository = repository;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<List<CatalogReadDto>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = await _repository.GetAllAsync(cancellationToken);
|
||||
return _mapper.Map<List<CatalogReadDto>>(items);
|
||||
}
|
||||
|
||||
public async Task<CatalogReadDto?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var item = await _repository.GetByIdAsync(id, cancellationToken);
|
||||
return item == null ? null : _mapper.Map<CatalogReadDto>(item);
|
||||
}
|
||||
|
||||
public async Task<CatalogReadDto?> CreateAsync(CatalogWriteDto dto, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var existing = await _repository.GetByTitleAsync(dto.CatTitle, cancellationToken);
|
||||
if (existing != null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var entity = _mapper.Map<VwmyCatalog>(dto);
|
||||
entity.AddedWho = "system";
|
||||
entity.AddedWhen = DateTime.UtcNow;
|
||||
entity.ChangedWho = "system";
|
||||
entity.ChangedWhen = DateTime.UtcNow;
|
||||
|
||||
var created = await _repository.InsertAsync(entity, cancellationToken);
|
||||
return _mapper.Map<CatalogReadDto>(created);
|
||||
}
|
||||
|
||||
public async Task<CatalogReadDto?> UpdateAsync(int id, CatalogWriteDto dto, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var existing = await _repository.GetByIdAsync(id, cancellationToken);
|
||||
if (existing == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var entity = _mapper.Map<VwmyCatalog>(dto);
|
||||
entity.Guid = id;
|
||||
entity.CatTitle = dto.UpdateProcedure == CatalogUpdateProcedure.Update ? existing.CatTitle : dto.CatTitle;
|
||||
entity.AddedWho = existing.AddedWho;
|
||||
entity.AddedWhen = existing.AddedWhen;
|
||||
entity.ChangedWho = "system";
|
||||
entity.ChangedWhen = DateTime.UtcNow;
|
||||
|
||||
var procedure = dto.UpdateProcedure;
|
||||
var updated = await _repository.UpdateAsync(id, entity, procedure, cancellationToken);
|
||||
return updated == null ? null : _mapper.Map<CatalogReadDto>(updated);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _repository.DeleteAsync(id, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using DbFirst.Application.Catalogs;
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.Catalogs.Commands;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using AutoMapper;
|
||||
using DbFirst.Application.Repositories;
|
||||
using DbFirst.Domain.Entities;
|
||||
using DbFirst.Domain.Repositories;
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.Catalogs.Commands;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using DbFirst.Domain.Repositories;
|
||||
using DbFirst.Application.Repositories;
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.Catalogs.Commands;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using DbFirst.Application.Catalogs;
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.Catalogs.Commands;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using AutoMapper;
|
||||
using DbFirst.Application.Repositories;
|
||||
using DbFirst.Domain.Entities;
|
||||
using DbFirst.Domain.Repositories;
|
||||
using DbFirst.Domain;
|
||||
using MediatR;
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
namespace DbFirst.Application.Catalogs;
|
||||
|
||||
//TODO: create generic service to reduce code duplication
|
||||
|
||||
/* Copilot's Response:
|
||||
A generic CRUD base service adds little value in your case:
|
||||
|
||||
Pros:
|
||||
• Less boilerplate for simple entities without special logic.
|
||||
• Uniform CRUD signatures.
|
||||
|
||||
Cons/Practical here:
|
||||
• Domain logic differs per entity(unique title check, setting audit fields, forbidding title changes, stored procs with output GUID).
|
||||
• Generic services tend to be diluted by virtual methods/hooks for special cases—ending up with per-entity overrides and little real gain.
|
||||
• With stored procedures and output parameters, the pattern doesn’t fit cleanly because operations aren’t symmetric (separate procs for insert/update/delete).
|
||||
|
||||
Conclusion: For this solution a generic service would be more overhead than benefit. If you later have multiple very similar entities without special logic,
|
||||
you could consider a lightweight generic interface/base; for now, the specialized service implementation is cleaner. */
|
||||
|
||||
public interface ICatalogService
|
||||
{
|
||||
Task<List<CatalogReadDto>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||
Task<CatalogReadDto?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||
Task<CatalogReadDto?> CreateAsync(CatalogWriteDto dto, CancellationToken cancellationToken = default);
|
||||
Task<CatalogReadDto?> UpdateAsync(int id, CatalogWriteDto dto, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteAsync(int id, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using AutoMapper;
|
||||
using DbFirst.Domain.Repositories;
|
||||
using DbFirst.Application.Repositories;
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.Catalogs.Queries;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using DbFirst.Application.Catalogs;
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.Catalogs.Queries;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using AutoMapper;
|
||||
using DbFirst.Domain.Repositories;
|
||||
using DbFirst.Application.Repositories;
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.Catalogs.Queries;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using DbFirst.Application.Catalogs;
|
||||
using MediatR;
|
||||
|
||||
namespace DbFirst.Application.Catalogs.Queries;
|
||||
|
||||
10
DbFirst.Application/Repositories/ICatalogRepository.cs
Normal file
10
DbFirst.Application/Repositories/ICatalogRepository.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using DbFirst.Domain;
|
||||
using DbFirst.Domain.Entities;
|
||||
|
||||
namespace DbFirst.Application.Repositories;
|
||||
|
||||
public interface ICatalogRepository : IRepository<VwmyCatalog>
|
||||
{
|
||||
Task<VwmyCatalog?> GetByTitleAsync(string title, CancellationToken cancellationToken = default);
|
||||
Task<VwmyCatalog?> UpdateAsync(int id, VwmyCatalog catalog, CatalogUpdateProcedure procedure, CancellationToken cancellationToken = default);
|
||||
}
|
||||
10
DbFirst.Application/Repositories/IRepository.cs
Normal file
10
DbFirst.Application/Repositories/IRepository.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace DbFirst.Application.Repositories;
|
||||
|
||||
public interface IRepository<T>
|
||||
{
|
||||
Task<List<T>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||
Task<T?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||
Task<T> InsertAsync(T entity, CancellationToken cancellationToken = default);
|
||||
Task<T?> UpdateAsync(int id, T entity, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteAsync(int id, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -6,8 +6,4 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Repositories\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
using DbFirst.Domain.Entities;
|
||||
using DbFirst.Domain;
|
||||
|
||||
namespace DbFirst.Domain.Repositories;
|
||||
|
||||
// TODO: instead of creating interface per entity, consider using generic repository pattern (eg. IRepository<T>) to reduce code duplication.
|
||||
|
||||
/* Copilot's Response:
|
||||
|
||||
A generic Repository<T> isn’t really worthwhile here:
|
||||
• Reads from the view are generic, but inserts/updates/deletes go through stored procedures with special parameters/output GUIDs.You’d need lots of exceptions/overrides—little gain.
|
||||
• Operations aren’t symmetric (separate procs for insert/update/delete with output handling and reload), so a one-size-fits-all CRUD pattern doesn’t fit well.
|
||||
• Better to keep the specialized repo.If you want reuse, extract small helpers (e.g., for proc calls/output parameters/reload) instead of forcing a generic repository. */
|
||||
|
||||
|
||||
/* Hakan's Response:
|
||||
* I think it still makes sense. An execute SQL method can be added to repository. The relevant SQL can be executed in the application layer.
|
||||
* Because the application layer is no longer dependent on a method like CreateAsync. It is now directly dependent on that procedure.
|
||||
*/
|
||||
|
||||
// TODO: move to application layer as a part of clean architecture
|
||||
|
||||
/* Copilot's Response:
|
||||
|
||||
• Common: Keep the interface in the Domain layer, implementation in Infrastructure. Domain defines the data access contract, Infrastructure provides the implementation.
|
||||
That matches typical DDD/Clean Architecture.
|
||||
• Alternative: Move the interface to the Application layer. Then only Application depends on Infrastructure, Domain stays isolated from infra contracts.
|
||||
Pro: Domain contains only models/rules.
|
||||
Con: If Domain logic needs repos, it would depend on Application, weakening the layer separation.
|
||||
Recommendation: Keep the interface in Domain. It’s conventional, clean, and avoids a Domain -> Application dependency. Implementation stays in Infrastructure. */
|
||||
|
||||
/* Hakan's Response:
|
||||
* I don't think there's any point in keeping repository interfaces in the domain layer. These interfaces will be used in the application layer, not the domain layer.
|
||||
* Furthermore, this is how they are used in the most popular clean architecture pattern.
|
||||
* See: https://github.com/jasontaylordev/CleanArchitecture/blob/main/src/Infrastructure/Identity/IdentityService.cs
|
||||
*/
|
||||
|
||||
public interface ICatalogRepository
|
||||
{
|
||||
Task<List<VwmyCatalog>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||
Task<VwmyCatalog?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||
Task<VwmyCatalog?> GetByTitleAsync(string title, CancellationToken cancellationToken = default);
|
||||
Task<VwmyCatalog> InsertAsync(VwmyCatalog catalog, CancellationToken cancellationToken = default);
|
||||
Task<VwmyCatalog?> UpdateAsync(int id, VwmyCatalog catalog, CatalogUpdateProcedure procedure, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteAsync(int id, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -1,49 +1,54 @@
|
||||
using DbFirst.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DbFirst.Infrastructure;
|
||||
|
||||
public partial class ApplicationDbContext : DbContext
|
||||
{
|
||||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
|
||||
private readonly TableConfigurations _config;
|
||||
|
||||
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IOptionsMonitor<TableConfigurations> configOptions)
|
||||
: base(options)
|
||||
{
|
||||
_config = configOptions.CurrentValue;
|
||||
}
|
||||
|
||||
public virtual DbSet<VwmyCatalog> VwmyCatalogs { get; set; }
|
||||
|
||||
// TODO: Configure column names on appsettings via IConfiguration
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
var catCfg = _config.VwmyCatalog;
|
||||
|
||||
modelBuilder.Entity<VwmyCatalog>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Guid);
|
||||
|
||||
entity.ToView("VWMY_CATALOG");
|
||||
entity.ToView(catCfg.ViewName);
|
||||
|
||||
entity.Property(e => e.Guid).HasColumnName("GUID");
|
||||
entity.Property(e => e.Guid).HasColumnName(catCfg.GuidColumnName);
|
||||
entity.Property(e => e.AddedWho)
|
||||
.HasMaxLength(30)
|
||||
.IsUnicode(false)
|
||||
.HasColumnName("ADDED_WHO");
|
||||
.HasColumnName(catCfg.AddedWhoColumnName);
|
||||
entity.Property(e => e.AddedWhen)
|
||||
.HasColumnType("datetime")
|
||||
.HasColumnName("ADDED_WHEN");
|
||||
.HasColumnName(catCfg.AddedWhenColumnName);
|
||||
entity.Property(e => e.CatString)
|
||||
.HasMaxLength(900)
|
||||
.IsUnicode(false)
|
||||
.HasColumnName("CAT_STRING");
|
||||
.HasColumnName(catCfg.CatStringColumnName);
|
||||
entity.Property(e => e.CatTitle)
|
||||
.HasMaxLength(100)
|
||||
.IsUnicode(false)
|
||||
.HasColumnName("CAT_TITLE");
|
||||
.HasColumnName(catCfg.CatTitleColumnName);
|
||||
entity.Property(e => e.ChangedWhen)
|
||||
.HasColumnType("datetime")
|
||||
.HasColumnName("CHANGED_WHEN");
|
||||
.HasColumnName(catCfg.ChangedWhenColumnName);
|
||||
entity.Property(e => e.ChangedWho)
|
||||
.HasMaxLength(30)
|
||||
.IsUnicode(false)
|
||||
.HasColumnName("CHANGED_WHO");
|
||||
.HasColumnName(catCfg.ChangedWhoColumnName);
|
||||
});
|
||||
|
||||
OnModelCreatingPartial(modelBuilder);
|
||||
|
||||
@@ -16,10 +16,12 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DbFirst.Domain\DbFirst.Domain.csproj" />
|
||||
<ProjectReference Include="..\DbFirst.Application\DbFirst.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -8,6 +8,7 @@ public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.Configure<TableConfigurations>(configuration.GetSection("TableConfigurations"));
|
||||
services.AddDbContext<ApplicationDbContext>(options =>
|
||||
options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));
|
||||
return services;
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
using Azure;
|
||||
using DbFirst.Domain;
|
||||
using DbFirst.Domain.Entities;
|
||||
using DbFirst.Domain.Repositories;
|
||||
using DbFirst.Application.Repositories;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Text;
|
||||
using System.Threading.Channels;
|
||||
using static Microsoft.EntityFrameworkCore.DbLoggerCategory;
|
||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||
using DbFirst.Domain;
|
||||
|
||||
namespace DbFirst.Infrastructure.Repositories;
|
||||
|
||||
@@ -115,6 +107,11 @@ public class CatalogRepository : ICatalogRepository
|
||||
return await _db.VwmyCatalogs.AsNoTracking().FirstOrDefaultAsync(x => x.Guid == guid, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<VwmyCatalog?> UpdateAsync(int id, VwmyCatalog catalog, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await UpdateAsync(id, catalog, CatalogUpdateProcedure.Update, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var exists = await _db.VwmyCatalogs.AsNoTracking().AnyAsync(x => x.Guid == id, cancellationToken);
|
||||
|
||||
19
DbFirst.Infrastructure/TableConfigurations.cs
Normal file
19
DbFirst.Infrastructure/TableConfigurations.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace DbFirst.Infrastructure
|
||||
{
|
||||
public class TableConfigurations
|
||||
{
|
||||
public VwmyCatalogConfiguration VwmyCatalog { get; set; } = new();
|
||||
}
|
||||
|
||||
public class VwmyCatalogConfiguration
|
||||
{
|
||||
public string ViewName { get; set; } = "VWMY_CATALOG";
|
||||
public string GuidColumnName { get; set; } = "GUID";
|
||||
public string CatTitleColumnName { get; set; } = "CAT_TITLE";
|
||||
public string CatStringColumnName { get; set; } = "CAT_STRING";
|
||||
public string AddedWhoColumnName { get; set; } = "ADDED_WHO";
|
||||
public string AddedWhenColumnName { get; set; } = "ADDED_WHEN";
|
||||
public string ChangedWhoColumnName { get; set; } = "CHANGED_WHO";
|
||||
public string ChangedWhenColumnName { get; set; } = "CHANGED_WHEN";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user