Compare commits

...

6 Commits

Author SHA1 Message Date
OlgunR
7a78a48d03 Remove Repositories folder reference from project file
Removed the <ItemGroup> entry for the "Repositories" folder in DbFirst.Domain.csproj. This change only affects the project file and does not delete the folder or its contents from the file system.
2026-01-19 17:06:11 +01:00
OlgunR
0b3249cb46 Remove CatalogService and ICatalogService implementations
Eliminated the catalog service layer by deleting both CatalogService.cs and ICatalogService.cs. This removes all catalog-related CRUD operations, mapping logic, repository interactions, and domain-specific checks. Also removed related comments and TODOs regarding generic services and CQRS.
2026-01-19 17:05:36 +01:00
OlgunR
17fdb6ed51 Move repository interfaces to Application layer
Refactored IRepository<T> and ICatalogRepository to reside in the DbFirst.Application layer instead of Domain. Updated namespaces, using statements, and all references in services and handlers. Adjusted csproj dependencies to reflect the new structure. Updated comments to clarify Clean Architecture rationale and improved separation of concerns.
2026-01-19 16:42:48 +01:00
OlgunR
166acea8b1 Introduce generic IRepository<T> and refactor repositories
Added a generic IRepository<T> interface for common CRUD operations and updated ICatalogRepository to inherit from it, removing redundant methods. Updated CatalogRepository to implement the new interface. Cleaned up DbFirst.Domain.csproj by removing an unused folder reference. These changes improve code reuse and align with clean architecture practices.
2026-01-19 16:36:08 +01:00
OlgunR
6c2b1884d2 Configurable EF view/column mapping via appsettings
Refactor ApplicationDbContext to use a configuration-driven approach for mapping view and column names, enabling dynamic mapping through appsettings.json. Add TableConfigurations classes, update DI registration, and include the necessary options package for configuration binding. This improves maintainability and flexibility for schema changes.
2026-01-19 16:25:08 +01:00
OlgunR
3653def773 Clean up and organize using statements across files
Removed unused and redundant using/import statements from multiple files, including command, repository, and Program.cs. No functional changes; this commit improves code clarity and organization.
2026-01-19 14:56:55 +01:00
22 changed files with 83 additions and 212 deletions

View File

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

View File

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

View File

@@ -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 doesnt fit cleanly because operations arent 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);
}
}

View File

@@ -1,4 +1,3 @@
using DbFirst.Application.Catalogs;
using MediatR;
namespace DbFirst.Application.Catalogs.Commands;

View File

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

View File

@@ -1,4 +1,4 @@
using DbFirst.Domain.Repositories;
using DbFirst.Application.Repositories;
using MediatR;
namespace DbFirst.Application.Catalogs.Commands;

View File

@@ -1,4 +1,3 @@
using DbFirst.Application.Catalogs;
using MediatR;
namespace DbFirst.Application.Catalogs.Commands;

View File

@@ -1,6 +1,6 @@
using AutoMapper;
using DbFirst.Application.Repositories;
using DbFirst.Domain.Entities;
using DbFirst.Domain.Repositories;
using DbFirst.Domain;
using MediatR;

View File

@@ -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 doesnt fit cleanly because operations arent 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);
}

View File

@@ -1,5 +1,5 @@
using AutoMapper;
using DbFirst.Domain.Repositories;
using DbFirst.Application.Repositories;
using MediatR;
namespace DbFirst.Application.Catalogs.Queries;

View File

@@ -1,4 +1,3 @@
using DbFirst.Application.Catalogs;
using MediatR;
namespace DbFirst.Application.Catalogs.Queries;

View File

@@ -1,5 +1,5 @@
using AutoMapper;
using DbFirst.Domain.Repositories;
using DbFirst.Application.Repositories;
using MediatR;
namespace DbFirst.Application.Catalogs.Queries;

View File

@@ -1,4 +1,3 @@
using DbFirst.Application.Catalogs;
using MediatR;
namespace DbFirst.Application.Catalogs.Queries;

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

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

View File

@@ -6,8 +6,4 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Folder Include="Repositories\" />
</ItemGroup>
</Project>

View File

@@ -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> isnt really worthwhile here:
• Reads from the view are generic, but inserts/updates/deletes go through stored procedures with special parameters/output GUIDs.Youd need lots of exceptions/overrides—little gain.
• Operations arent symmetric (separate procs for insert/update/delete with output handling and reload), so a one-size-fits-all CRUD pattern doesnt 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. Its 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);
}

View File

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

View File

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

View File

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

View File

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

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