modules/ai-guide/Write-AiGuide-Endpoints.ps1
|
param( [string] $ProjectName, [string] $OrmMode, [string] $DbProvider = "MSSQL", [string] $FrontendMode, [string[]] $Entities = @("User", "Product"), [string] $OutputPath, [bool] $SkipTests = $false, [bool] $SkipCi = $false, [bool] $SkipFrontend = $false, [bool] $Observability = $false, [string] $IdeConfig = "VSCode" ) . (Join-Path (Split-Path $MyInvocation.MyCommand.Path) "..\core\Utils.ps1") $guideDir = Join-Path $OutputPath "ai-guide" function Write-Guide($fileName, $content) { Write-CoreFile (Join-Path $guideDir $fileName) $content } $allEntitiesStr = $Entities -join ", " $ormRepoNote = switch ($OrmMode) { "Dapper" { "Dapper ile raw SQL yaz. SQL provider'a gore INSERT syntax farklidir (bkz. orm-guide.md)." } "EFCore" { "EF Core: ``_context.Set<T>().AddAsync()`` + ``SaveChangesAsync()`` kullan." } "Hybrid" { "Yazma: Dapper (IDbConnection) | Okuma: EF Core (_context.AsNoTracking()). GetPagedAsync EF Core ile yapilir." } } $dbNote = switch ($DbProvider) { "Postgres" { "PostgreSQL (Npgsql)" } "SQLite" { "SQLite (dosya tabanli)" } default { "MS SQL Server" } } $repoImplNote = switch ($OrmMode) { "Dapper" { @" ``````csharp using Dapper; using System.Data; using $ProjectName.Domain.Common; using $ProjectName.Domain.Entities; using $ProjectName.Domain.Interfaces; namespace $ProjectName.Persistence.Repositories; public class CategoryRepository : ICategoryRepository { private readonly IDbConnection _connection; public CategoryRepository(IDbConnection connection) => _connection = connection; public async Task<IEnumerable<Category>> GetAllAsync() => await _connection.QueryAsync<Category>("SELECT * FROM Categories WHERE IsDeleted=0 ORDER BY CreatedAt DESC"); public async Task<PagedResult<Category>> GetPagedAsync(PagedQuery query) { var total = await _connection.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM Categories WHERE IsDeleted=0"); var items = await _connection.QueryAsync<Category>( "SELECT * FROM Categories WHERE IsDeleted=0 ORDER BY CreatedAt DESC OFFSET @Skip ROWS FETCH NEXT @Size ROWS ONLY", new { query.Skip, query.Size }); return new PagedResult<Category> { Items = items, TotalCount = total, Page = query.Page, Size = query.Size }; } public async Task<int> AddAsync(Category c) { const string sql = "INSERT INTO Categories (Name, CreatedAt, UpdatedAt, IsDeleted) OUTPUT INSERTED.Id VALUES (@Name, @CreatedAt, @UpdatedAt, 0)"; return await _connection.ExecuteScalarAsync<int>(sql, c); } public async Task DeleteAsync(int id) => await _connection.ExecuteAsync("UPDATE Categories SET IsDeleted=1, DeletedAt=GETUTCDATE() WHERE Id=@id", new { id }); } `````` "@ } "EFCore" { @" ``````csharp using Microsoft.EntityFrameworkCore; using $ProjectName.Domain.Common; using $ProjectName.Domain.Entities; using $ProjectName.Domain.Interfaces; using $ProjectName.Persistence.Context; namespace $ProjectName.Persistence.Repositories; public class CategoryRepository : ICategoryRepository { private readonly AppDbContext _context; public CategoryRepository(AppDbContext context) => _context = context; public async Task<IEnumerable<Category>> GetAllAsync() => await _context.Categories.AsNoTracking().OrderByDescending(c => c.CreatedAt).ToListAsync(); public async Task<PagedResult<Category>> GetPagedAsync(PagedQuery query) { var total = await _context.Categories.CountAsync(); var items = await _context.Categories.Skip(query.Skip).Take(query.Size).ToListAsync(); return new PagedResult<Category> { Items = items, TotalCount = total, Page = query.Page, Size = query.Size }; } public async Task<int> AddAsync(Category c) { await _context.Categories.AddAsync(c); await _context.SaveChangesAsync(); return c.Id; } public async Task DeleteAsync(int id) { var entity = await _context.Categories.FindAsync(id); if (entity != null) { entity.IsDeleted = true; entity.DeletedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); } } } `````` "@ } "Hybrid" { @" ``````csharp using Dapper; using System.Data; using Microsoft.EntityFrameworkCore; using $ProjectName.Domain.Common; using $ProjectName.Domain.Entities; using $ProjectName.Domain.Interfaces; using $ProjectName.Persistence.Context; namespace $ProjectName.Persistence.Repositories; public class CategoryRepository : ICategoryRepository { private readonly AppDbContext _ctx; private readonly IDbConnection _conn; public CategoryRepository(AppDbContext ctx, IDbConnection conn) { _ctx = ctx; _conn = conn; } // Okuma: EF Core public async Task<IEnumerable<Category>> GetAllAsync() => await _ctx.Categories.AsNoTracking().OrderByDescending(c => c.CreatedAt).ToListAsync(); public async Task<PagedResult<Category>> GetPagedAsync(PagedQuery query) { var total = await _ctx.Categories.CountAsync(); var items = await _ctx.Categories.Skip(query.Skip).Take(query.Size).ToListAsync(); return new PagedResult<Category> { Items = items, TotalCount = total, Page = query.Page, Size = query.Size }; } // Yazma: Dapper public async Task<int> AddAsync(Category c) { const string sql = "INSERT INTO Categories (Name, CreatedAt, UpdatedAt, IsDeleted) OUTPUT INSERTED.Id VALUES (@Name, @CreatedAt, @UpdatedAt, 0)"; return await _conn.ExecuteScalarAsync<int>(sql, c); } public async Task DeleteAsync(int id) { var entity = await _ctx.Categories.FindAsync(id); if (entity != null) { entity.IsDeleted = true; entity.DeletedAt = DateTime.UtcNow; await _ctx.SaveChangesAsync(); } } } `````` "@ } } $dbTableNote = switch ($DbProvider) { "Postgres" { @" ``````sql CREATE TABLE IF NOT EXISTS "Categories" ( "Id" SERIAL PRIMARY KEY, "Name" VARCHAR(200) NOT NULL, "IsDeleted" BOOLEAN NOT NULL DEFAULT FALSE, "DeletedAt" TIMESTAMP, "CreatedAt" TIMESTAMP NOT NULL DEFAULT NOW(), "UpdatedAt" TIMESTAMP NOT NULL DEFAULT NOW() ); `````` "@ } "SQLite" { @" ``````sql CREATE TABLE IF NOT EXISTS Categories ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Name TEXT NOT NULL, IsDeleted INTEGER NOT NULL DEFAULT 0, DeletedAt TEXT, CreatedAt TEXT NOT NULL DEFAULT (datetime('now')), UpdatedAt TEXT NOT NULL DEFAULT (datetime('now')) ); `````` "@ } default { @" ``````sql CREATE TABLE Categories ( Id INT IDENTITY(1,1) PRIMARY KEY, Name NVARCHAR(200) NOT NULL, IsDeleted BIT NOT NULL DEFAULT 0, DeletedAt DATETIME2, CreatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE(), UpdatedAt DATETIME2 NOT NULL DEFAULT GETUTCDATE() ); `````` "@ } } $efContextNote = if ($OrmMode -ne "Dapper") { @" **AppDbContext.cs icine ekle:** ``````csharp public DbSet<Category> Categories => Set<Category>(); // OnModelCreating icinde - soft delete global filter: builder.Entity<Category>().HasQueryFilter(e => !e.IsDeleted); `````` **EF Core Migration calistir:** ``````bash dotnet ef migrations add AddCategory \ --project src/$ProjectName.Persistence \ --startup-project src/$ProjectName.WebApi dotnet ef database update \ --project src/$ProjectName.Persistence \ --startup-project src/$ProjectName.WebApi `````` "@ } else { "" } # ─── add-feature.md ─────────────────────────────────────── Write-Guide "add-feature.md" @" # Yeni Ozellik (Entity) Ekleme Rehberi Bu rehber, projeye yeni bir entity eklemek icin gereken tum adimlarini sirayla aciklar. Ornek entity: **Category** > Mevcut entityler: $allEntitiesStr > Bunlar ayni yapiyla uretilmistir - referans olarak kullanabilirsin. --- ## Adim 1 - Domain: Entity ``````csharp // src/$ProjectName.Domain/Entities/Category.cs namespace $ProjectName.Domain.Entities; public class Category : BaseEntity { public string Name { get; set; } = string.Empty; // BaseEntity'den: Id, CreatedAt, UpdatedAt, IsDeleted, DeletedAt } `````` ## Adim 2 - Domain: Interface ``````csharp // src/$ProjectName.Domain/Interfaces/ICategoryRepository.cs using $ProjectName.Domain.Common; namespace $ProjectName.Domain.Interfaces; public interface ICategoryRepository : IRepository<Category> { // Ek metotlar gerekiyorsa buraya ekle } `````` > ``IRepository<T>`` zaten sunlari icerir: > ``GetByIdAsync``, ``GetAllAsync``, ``GetPagedAsync``, ``AddAsync``, ``UpdateAsync``, ``DeleteAsync`` ## Adim 3 - Application: DTO ``````csharp // src/$ProjectName.Application/DTOs/CategoryDto.cs namespace $ProjectName.Application.DTOs; public record CategoryDto(int Id, string Name, DateTime CreatedAt); `````` ## Adim 4 - Application: AutoMapper ``````csharp // src/$ProjectName.Application/Mappings/CategoryProfile.cs using AutoMapper; using $ProjectName.Domain.Entities; using $ProjectName.Application.DTOs; namespace $ProjectName.Application.Mappings; public class CategoryProfile : Profile { public CategoryProfile() { CreateMap<Category, CategoryDto>(); } } `````` ## Adim 5 - Application: CQRS Queries ``````csharp // GetAllCategoriesQuery.cs public record GetAllCategoriesQuery : IRequest<IEnumerable<CategoryDto>>; public class GetAllCategoriesQueryHandler : IRequestHandler<GetAllCategoriesQuery, IEnumerable<CategoryDto>> { private readonly ICategoryRepository _repo; private readonly IMapper _mapper; public GetAllCategoriesQueryHandler(ICategoryRepository repo, IMapper mapper) { _repo = repo; _mapper = mapper; } public async Task<IEnumerable<CategoryDto>> Handle(GetAllCategoriesQuery request, CancellationToken ct) => _mapper.Map<IEnumerable<CategoryDto>>(await _repo.GetAllAsync()); } // GetPagedCategoriesQuery.cs public record GetPagedCategoriesQuery(int Page, int Size) : IRequest<PagedResult<CategoryDto>>; public class GetPagedCategoriesQueryHandler : IRequestHandler<GetPagedCategoriesQuery, PagedResult<CategoryDto>> { private readonly ICategoryRepository _repo; private readonly IMapper _mapper; public GetPagedCategoriesQueryHandler(ICategoryRepository repo, IMapper mapper) { _repo = repo; _mapper = mapper; } public async Task<PagedResult<CategoryDto>> Handle(GetPagedCategoriesQuery request, CancellationToken ct) { var query = new PagedQuery { Page = request.Page, Size = request.Size }; var result = await _repo.GetPagedAsync(query); return new PagedResult<CategoryDto> { Items = _mapper.Map<IEnumerable<CategoryDto>>(result.Items), TotalCount = result.TotalCount, Page = result.Page, Size = result.Size }; } } `````` ## Adim 6 - Application: CQRS Commands ``````csharp // AddCategoryCommand.cs public record AddCategoryCommand(string Name) : IRequest<int>; public class AddCategoryCommandHandler : IRequestHandler<AddCategoryCommand, int> { private readonly ICategoryRepository _repo; public AddCategoryCommandHandler(ICategoryRepository repo) => _repo = repo; public async Task<int> Handle(AddCategoryCommand request, CancellationToken ct) { var category = new Category { Name = request.Name, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; return await _repo.AddAsync(category); } } // AddCategoryCommandValidator.cs public class AddCategoryCommandValidator : AbstractValidator<AddCategoryCommand> { public AddCategoryCommandValidator() { RuleFor(x => x.Name).NotEmpty().MaximumLength(200); } } // DeleteCategoryCommand.cs public record DeleteCategoryCommand(int Id) : IRequest<Unit>; public class DeleteCategoryCommandHandler : IRequestHandler<DeleteCategoryCommand, Unit> { private readonly ICategoryRepository _repo; public DeleteCategoryCommandHandler(ICategoryRepository repo) => _repo = repo; public async Task<Unit> Handle(DeleteCategoryCommand request, CancellationToken ct) { await _repo.DeleteAsync(request.Id); // Soft delete - fiziksel silme yok return Unit.Value; } } `````` ## Adim 7 - Persistence: Repository ($OrmMode) Not: $ormRepoNote $repoImplNote ## Adim 8 - Persistence: DI Kaydı ``````csharp // ServiceRegistration.cs icine ekle: services.AddScoped<ICategoryRepository, CategoryRepository>(); `````` ## Adim 9 - WebApi: Controller ``````csharp // src/$ProjectName.WebApi/Controllers/CategoriesController.cs [ApiController] [Route("api/[controller]")] [Authorize] public class CategoriesController : ControllerBase { private readonly IMediator _mediator; public CategoriesController(IMediator mediator) => _mediator = mediator; [HttpGet] public async Task<IActionResult> GetAll() => Ok(ApiResponse<IEnumerable<CategoryDto>>.Ok(await _mediator.Send(new GetAllCategoriesQuery()))); [HttpGet("paged")] public async Task<IActionResult> GetPaged([FromQuery] int page = 1, [FromQuery] int size = 20) => Ok(ApiResponse<PagedResult<CategoryDto>>.Ok(await _mediator.Send(new GetPagedCategoriesQuery(page, size)))); [HttpPost] public async Task<IActionResult> Add([FromBody] AddCategoryCommand cmd) => Ok(ApiResponse<int>.Ok(await _mediator.Send(cmd), "Category eklendi.")); [HttpDelete("{id}")] [Authorize(Policy = "AdminOnly")] public async Task<IActionResult> Delete(int id) => Ok(ApiResponse<Unit>.Ok(await _mediator.Send(new DeleteCategoryCommand(id)))); } `````` ## Adim 10 - Veritabani ($dbNote) $dbTableNote $efContextNote --- ## Adim 11 - Dev Log Guncelle Her yeni entity veya mimari karar sonrasinda ``ai-guide/devlog.md`` dosyasina giris ekle: ``````markdown ## [YYYY-AA-GG] <Entity/Ozellik adi> eklendi **Neden:** <Neden bu entity/ozellik gerekti> **Nasil:** <Hangi adimlar izlendi, hangi kararlar alindi> **Karar:** <Dikkat edilmesi gereken onemli tercih veya kisitlama> **Etkilenen:** <Eklenen/degistirilen dosyalar> `````` Bu adim isteğe baglidir ama AI asistanin gelecekte daha iyi baglam kurmasini saglar. "@ # ─── orm-guide.md ───────────────────────────────────────── $dbProviderNote = switch ($DbProvider) { "Postgres" { "**PostgreSQL (Npgsql):** Tablo/sutun icin cift tirnak veya snake_case. ``RETURNING Id`` ile yeni ID. ``OFFSET/LIMIT`` ile sayfalama." } "SQLite" { "**SQLite:** ``last_insert_rowid()`` ile yeni ID. ``LIMIT/OFFSET`` ile sayfalama. Boolean icin INTEGER (0/1) kullanilir." } default { "**MS SQL Server:** ``OUTPUT INSERTED.Id`` ile yeni ID. ``OFFSET/FETCH NEXT`` ile sayfalama. Bit ile Boolean." } } $insertSqlSnippet = if ($DbProvider -eq "Postgres") { @" ``````csharp const string sql = @" INSERT INTO ""Products"" (""Name"", ""Price"", ""CreatedAt"", ""UpdatedAt"", ""IsDeleted"") VALUES (@Name, @Price, @CreatedAt, @UpdatedAt, false) RETURNING ""Id"" "; product.Id = await _connection.ExecuteScalarAsync<int>(sql, product); `````` "@ } elseif ($DbProvider -eq "SQLite") { @" ``````csharp const string sql = @" INSERT INTO Products (Name, Price, CreatedAt, UpdatedAt, IsDeleted) VALUES (@Name, @Price, @CreatedAt, @UpdatedAt, 0); SELECT last_insert_rowid();"; product.Id = await _connection.ExecuteScalarAsync<int>(sql, product); `````` "@ } else { @" ``````csharp const string sql = @" INSERT INTO Products (Name, Price, CreatedAt, UpdatedAt, IsDeleted) OUTPUT INSERTED.Id VALUES (@Name, @Price, @CreatedAt, @UpdatedAt, 0)"; product.Id = await _connection.ExecuteScalarAsync<int>(sql, product); `````` "@ } $pagedSqlSnippet = if ($DbProvider -eq "Postgres") { @" ``````csharp var total = await _conn.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM ""Products"" WHERE ""IsDeleted""=false"); var items = await _conn.QueryAsync<Product>( @"SELECT * FROM ""Products"" WHERE ""IsDeleted""=false ORDER BY ""CreatedAt"" DESC LIMIT @Size OFFSET @Skip", new { query.Size, query.Skip }); `````` "@ } elseif ($DbProvider -eq "SQLite") { @" ``````csharp var total = await _conn.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM Products WHERE IsDeleted=0"); var items = await _conn.QueryAsync<Product>( "SELECT * FROM Products WHERE IsDeleted=0 ORDER BY CreatedAt DESC LIMIT @Size OFFSET @Skip", new { query.Size, query.Skip }); `````` "@ } else { @" ``````csharp var total = await _conn.ExecuteScalarAsync<int>("SELECT COUNT(*) FROM Products WHERE IsDeleted=0"); var items = await _conn.QueryAsync<Product>( "SELECT * FROM Products WHERE IsDeleted=0 ORDER BY CreatedAt DESC OFFSET @Skip ROWS FETCH NEXT @Size ROWS ONLY", new { query.Skip, query.Size }); `````` "@ } $ormGuide = switch ($OrmMode) { "Dapper" { @" # ORM Kilavuzu - Dapper ## Veritabani: $dbNote $dbProviderNote ## INSERT (Yeni ID Alma) $insertSqlSnippet ## SELECT (Tum Kayitlar - Soft Delete Filtreli) $(if ($DbProvider -eq "Postgres") { @" ``````csharp const string sql = @"SELECT * FROM ""Products"" WHERE ""IsDeleted""=false ORDER BY ""CreatedAt"" DESC"; return (await _connection.QueryAsync<Product>(sql)).ToList(); `````` "@ } else { @" ``````csharp const string sql = "SELECT * FROM Products WHERE IsDeleted=0 ORDER BY CreatedAt DESC"; return (await _connection.QueryAsync<Product>(sql)).ToList(); `````` "@ }) ## GetPagedAsync $pagedSqlSnippet ``````csharp return new PagedResult<Product> { Items = items, TotalCount = total, Page = query.Page, Size = query.Size }; `````` ## Soft Delete (UPDATE - fiziksel silme yok) $(if ($DbProvider -eq "Postgres") { @" ``````csharp await _connection.ExecuteAsync( @"UPDATE ""Products"" SET ""IsDeleted""=true, ""DeletedAt""=NOW() WHERE ""Id""=@id", new { id }); `````` "@ } elseif ($DbProvider -eq "SQLite") { @" ``````csharp await _connection.ExecuteAsync( "UPDATE Products SET IsDeleted=1, DeletedAt=datetime('now') WHERE Id=@id", new { id }); `````` "@ } else { @" ``````csharp await _connection.ExecuteAsync( "UPDATE Products SET IsDeleted=1, DeletedAt=GETUTCDATE() WHERE Id=@id", new { id }); `````` "@ }) ## UPDATE ``````csharp await _connection.ExecuteAsync( "UPDATE Products SET Name=@Name, Price=@Price, UpdatedAt=@UpdatedAt WHERE Id=@Id AND IsDeleted=0", product); `````` "@ } "EFCore" { @" # ORM Kilavuzu - EF Core 8 ## Veritabani: $dbNote $dbProviderNote ## SELECT (Global Query Filter - IsDeleted=true olanlari otomatik gizler) ``````csharp // Normal sorgu - silinmisleri getirmez (HasQueryFilter devrede) return await _context.Products.AsNoTracking().OrderByDescending(p => p.CreatedAt).ToListAsync(); // Silinmisleri de getirmek icin: return await _context.Products.IgnoreQueryFilters().ToListAsync(); `````` ## GetPagedAsync ``````csharp public async Task<PagedResult<Product>> GetPagedAsync(PagedQuery query) { var total = await _context.Products.CountAsync(); var items = await _context.Products.Skip(query.Skip).Take(query.Size).ToListAsync(); return new PagedResult<Product> { Items = items, TotalCount = total, Page = query.Page, Size = query.Size }; } `````` ## INSERT ``````csharp await _context.Products.AddAsync(product); await _context.SaveChangesAsync(); return product.Id; `````` ## Soft Delete ``````csharp var entity = await _context.Products.FindAsync(id); if (entity != null) { entity.IsDeleted = true; entity.DeletedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); } `````` ## LINQ Ornekleri ``````csharp // Filtreleme await _context.Products.Where(p => p.Name.Contains(search)).ToListAsync(); // Include (iliskili entity) await _context.Orders.Include(o => o.Product).ToListAsync(); // Projeksiyon (DTO'ya direkt map) await _context.Products.Select(p => new ProductDto(p.Id, p.Name, p.CreatedAt)).ToListAsync(); `````` "@ } "Hybrid" { @" # ORM Kilavuzu - Hybrid (Dapper + EF Core) ## Veritabani: $dbNote $dbProviderNote ## Hangi ORM Ne Zaman? | Operasyon | ORM | Neden | |--------------------|----------|-----------------------------------------------------| | GetAllAsync | EF Core | LINQ, AsNoTracking, global soft delete filter | | GetByIdAsync | EF Core | FindAsync cache avantaji | | GetPagedAsync | EF Core | Skip/Take kolayligi, global filter otomatik calisir | | AddAsync | Dapper | Anlik INSERT + ID donusu | | UpdateAsync | Dapper | Performansli bulk update | | DeleteAsync | EF Core | Soft delete: entity yukle, flag set et, kaydet | ## Okuma: EF Core ``````csharp // Soft delete global filter otomatik devrede var all = await _ctx.Products.AsNoTracking().ToListAsync(); // Sayfalama var paged = await _ctx.Products.Skip(query.Skip).Take(query.Size).ToListAsync(); `````` ## Yazma: Dapper $insertSqlSnippet ## Soft Delete: EF Core ``````csharp var entity = await _ctx.Products.FindAsync(id); if (entity != null) { entity.IsDeleted = true; entity.DeletedAt = DateTime.UtcNow; await _ctx.SaveChangesAsync(); } `````` "@ } } Write-Guide "orm-guide.md" $ormGuide |