From 85b9b0b51a1f3cee143bb41c2ba2a24e0719965f Mon Sep 17 00:00:00 2001 From: OlgunR Date: Wed, 4 Feb 2026 11:39:58 +0100 Subject: [PATCH] Add MassData API with CQRS, repository, and DbContext Introduce MassData feature with new API endpoints for querying and upserting records by customer name. Add DTOs, AutoMapper profile, MediatR CQRS handlers, repository pattern, and MassDataDbContext. Register new services in DI and add MassDataConnection to configuration. Upsert uses stored procedure. Enables full CRUD for Massdata via dedicated API. --- DbFirst.API/Controllers/MassDataController.cs | 46 +++++++++++++ DbFirst.API/Program.cs | 1 + DbFirst.API/appsettings.json | 3 +- .../UpsertMassDataByCustomerNameCommand.cs | 5 ++ .../UpsertMassDataByCustomerNameHandler.cs | 24 +++++++ .../MassData/MassDataProfile.cs | 12 ++++ .../MassData/MassDataReadDto.cs | 12 ++++ .../MassData/MassDataWriteDto.cs | 9 +++ .../MassData/Queries/GetAllMassDataHandler.cs | 23 +++++++ .../MassData/Queries/GetAllMassDataQuery.cs | 5 ++ .../GetMassDataByCustomerNameHandler.cs | 23 +++++++ .../Queries/GetMassDataByCustomerNameQuery.cs | 5 ++ .../Repositories/IMassDataRepository.cs | 10 +++ DbFirst.Domain/Entities/Massdata.cs | 12 ++++ DbFirst.Infrastructure/DependencyInjection.cs | 4 ++ DbFirst.Infrastructure/MassDataDbContext.cs | 44 +++++++++++++ .../Repositories/MassDataRepository.cs | 66 +++++++++++++++++++ 17 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 DbFirst.API/Controllers/MassDataController.cs create mode 100644 DbFirst.Application/MassData/Commands/UpsertMassDataByCustomerNameCommand.cs create mode 100644 DbFirst.Application/MassData/Commands/UpsertMassDataByCustomerNameHandler.cs create mode 100644 DbFirst.Application/MassData/MassDataProfile.cs create mode 100644 DbFirst.Application/MassData/MassDataReadDto.cs create mode 100644 DbFirst.Application/MassData/MassDataWriteDto.cs create mode 100644 DbFirst.Application/MassData/Queries/GetAllMassDataHandler.cs create mode 100644 DbFirst.Application/MassData/Queries/GetAllMassDataQuery.cs create mode 100644 DbFirst.Application/MassData/Queries/GetMassDataByCustomerNameHandler.cs create mode 100644 DbFirst.Application/MassData/Queries/GetMassDataByCustomerNameQuery.cs create mode 100644 DbFirst.Application/Repositories/IMassDataRepository.cs create mode 100644 DbFirst.Domain/Entities/Massdata.cs create mode 100644 DbFirst.Infrastructure/MassDataDbContext.cs create mode 100644 DbFirst.Infrastructure/Repositories/MassDataRepository.cs diff --git a/DbFirst.API/Controllers/MassDataController.cs b/DbFirst.API/Controllers/MassDataController.cs new file mode 100644 index 0000000..96d09f3 --- /dev/null +++ b/DbFirst.API/Controllers/MassDataController.cs @@ -0,0 +1,46 @@ +using DbFirst.Application.MassData; +using DbFirst.Application.MassData.Commands; +using DbFirst.Application.MassData.Queries; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace DbFirst.API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class MassDataController : ControllerBase +{ + private readonly IMediator _mediator; + + public MassDataController(IMediator mediator) + { + _mediator = mediator; + } + + [HttpGet] + public async Task>> GetAll([FromQuery] int? skip, [FromQuery] int? take, CancellationToken cancellationToken) + { + var resolvedTake = take is null or <= 0 ? 200 : take; + var result = await _mediator.Send(new GetAllMassDataQuery(skip, resolvedTake), cancellationToken); + return Ok(result); + } + + [HttpGet("{customerName}")] + public async Task> GetByCustomerName(string customerName, CancellationToken cancellationToken) + { + var result = await _mediator.Send(new GetMassDataByCustomerNameQuery(customerName), cancellationToken); + if (result == null) + { + return NotFound(); + } + + return Ok(result); + } + + [HttpPost("upsert")] + public async Task> Upsert(MassDataWriteDto dto, CancellationToken cancellationToken) + { + var result = await _mediator.Send(new UpsertMassDataByCustomerNameCommand(dto), cancellationToken); + return Ok(result); + } +} diff --git a/DbFirst.API/Program.cs b/DbFirst.API/Program.cs index 58e1536..98c6e0b 100644 --- a/DbFirst.API/Program.cs +++ b/DbFirst.API/Program.cs @@ -52,6 +52,7 @@ builder.Services.AddInfrastructure(builder.Configuration); builder.Services.AddApplication(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddDevExpressControls(); builder.Services.AddSignalR(); diff --git a/DbFirst.API/appsettings.json b/DbFirst.API/appsettings.json index d34692d..02a6d09 100644 --- a/DbFirst.API/appsettings.json +++ b/DbFirst.API/appsettings.json @@ -1,6 +1,7 @@ { "ConnectionStrings": { - "DefaultConnection": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;TrustServerCertificate=True;" + "DefaultConnection": "Server=SDD-VMP04-SQL17\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;TrustServerCertificate=True;", + "MassDataConnection": "Server=SDD-VMP04-SQL19\\DD_DEVELOP01;Database=DD_ECM;User Id=sa;Password=dd;TrustServerCertificate=True;" }, "Dashboard": { "BaseUrl": "https://localhost:7204" diff --git a/DbFirst.Application/MassData/Commands/UpsertMassDataByCustomerNameCommand.cs b/DbFirst.Application/MassData/Commands/UpsertMassDataByCustomerNameCommand.cs new file mode 100644 index 0000000..f719910 --- /dev/null +++ b/DbFirst.Application/MassData/Commands/UpsertMassDataByCustomerNameCommand.cs @@ -0,0 +1,5 @@ +using MediatR; + +namespace DbFirst.Application.MassData.Commands; + +public record UpsertMassDataByCustomerNameCommand(MassDataWriteDto Dto) : IRequest; diff --git a/DbFirst.Application/MassData/Commands/UpsertMassDataByCustomerNameHandler.cs b/DbFirst.Application/MassData/Commands/UpsertMassDataByCustomerNameHandler.cs new file mode 100644 index 0000000..e301fc9 --- /dev/null +++ b/DbFirst.Application/MassData/Commands/UpsertMassDataByCustomerNameHandler.cs @@ -0,0 +1,24 @@ +using AutoMapper; +using DbFirst.Application.Repositories; +using MediatR; + +namespace DbFirst.Application.MassData.Commands; + +public class UpsertMassDataByCustomerNameHandler : IRequestHandler +{ + private readonly IMassDataRepository _repository; + private readonly IMapper _mapper; + + public UpsertMassDataByCustomerNameHandler(IMassDataRepository repository, IMapper mapper) + { + _repository = repository; + _mapper = mapper; + } + + public async Task Handle(UpsertMassDataByCustomerNameCommand request, CancellationToken cancellationToken) + { + var dto = request.Dto; + var updated = await _repository.UpsertByCustomerNameAsync(dto.CustomerName, dto.Amount, dto.StatusFlag, dto.Category, cancellationToken); + return _mapper.Map(updated); + } +} diff --git a/DbFirst.Application/MassData/MassDataProfile.cs b/DbFirst.Application/MassData/MassDataProfile.cs new file mode 100644 index 0000000..b0f260c --- /dev/null +++ b/DbFirst.Application/MassData/MassDataProfile.cs @@ -0,0 +1,12 @@ +using AutoMapper; +using DbFirst.Domain.Entities; + +namespace DbFirst.Application.MassData; + +public class MassDataProfile : Profile +{ + public MassDataProfile() + { + CreateMap(); + } +} diff --git a/DbFirst.Application/MassData/MassDataReadDto.cs b/DbFirst.Application/MassData/MassDataReadDto.cs new file mode 100644 index 0000000..db2fa09 --- /dev/null +++ b/DbFirst.Application/MassData/MassDataReadDto.cs @@ -0,0 +1,12 @@ +namespace DbFirst.Application.MassData; + +public class MassDataReadDto +{ + public int Id { get; set; } + public string CustomerName { get; set; } = string.Empty; + public decimal Amount { get; set; } + public string Category { get; set; } = string.Empty; + public bool StatusFlag { get; set; } + public DateTime AddedWhen { get; set; } + public DateTime? ChangedWhen { get; set; } +} diff --git a/DbFirst.Application/MassData/MassDataWriteDto.cs b/DbFirst.Application/MassData/MassDataWriteDto.cs new file mode 100644 index 0000000..e1cacb5 --- /dev/null +++ b/DbFirst.Application/MassData/MassDataWriteDto.cs @@ -0,0 +1,9 @@ +namespace DbFirst.Application.MassData; + +public class MassDataWriteDto +{ + public string CustomerName { get; set; } = string.Empty; + public decimal Amount { get; set; } + public string Category { get; set; } = string.Empty; + public bool StatusFlag { get; set; } +} diff --git a/DbFirst.Application/MassData/Queries/GetAllMassDataHandler.cs b/DbFirst.Application/MassData/Queries/GetAllMassDataHandler.cs new file mode 100644 index 0000000..e3003d0 --- /dev/null +++ b/DbFirst.Application/MassData/Queries/GetAllMassDataHandler.cs @@ -0,0 +1,23 @@ +using AutoMapper; +using DbFirst.Application.Repositories; +using MediatR; + +namespace DbFirst.Application.MassData.Queries; + +public class GetAllMassDataHandler : IRequestHandler> +{ + private readonly IMassDataRepository _repository; + private readonly IMapper _mapper; + + public GetAllMassDataHandler(IMassDataRepository repository, IMapper mapper) + { + _repository = repository; + _mapper = mapper; + } + + public async Task> Handle(GetAllMassDataQuery request, CancellationToken cancellationToken) + { + var items = await _repository.GetAllAsync(request.Skip, request.Take, cancellationToken); + return _mapper.Map>(items); + } +} diff --git a/DbFirst.Application/MassData/Queries/GetAllMassDataQuery.cs b/DbFirst.Application/MassData/Queries/GetAllMassDataQuery.cs new file mode 100644 index 0000000..84b350a --- /dev/null +++ b/DbFirst.Application/MassData/Queries/GetAllMassDataQuery.cs @@ -0,0 +1,5 @@ +using MediatR; + +namespace DbFirst.Application.MassData.Queries; + +public record GetAllMassDataQuery(int? Skip, int? Take) : IRequest>; diff --git a/DbFirst.Application/MassData/Queries/GetMassDataByCustomerNameHandler.cs b/DbFirst.Application/MassData/Queries/GetMassDataByCustomerNameHandler.cs new file mode 100644 index 0000000..7d518c9 --- /dev/null +++ b/DbFirst.Application/MassData/Queries/GetMassDataByCustomerNameHandler.cs @@ -0,0 +1,23 @@ +using AutoMapper; +using DbFirst.Application.Repositories; +using MediatR; + +namespace DbFirst.Application.MassData.Queries; + +public class GetMassDataByCustomerNameHandler : IRequestHandler +{ + private readonly IMassDataRepository _repository; + private readonly IMapper _mapper; + + public GetMassDataByCustomerNameHandler(IMassDataRepository repository, IMapper mapper) + { + _repository = repository; + _mapper = mapper; + } + + public async Task Handle(GetMassDataByCustomerNameQuery request, CancellationToken cancellationToken) + { + var item = await _repository.GetByCustomerNameAsync(request.CustomerName, cancellationToken); + return item == null ? null : _mapper.Map(item); + } +} diff --git a/DbFirst.Application/MassData/Queries/GetMassDataByCustomerNameQuery.cs b/DbFirst.Application/MassData/Queries/GetMassDataByCustomerNameQuery.cs new file mode 100644 index 0000000..26d75a6 --- /dev/null +++ b/DbFirst.Application/MassData/Queries/GetMassDataByCustomerNameQuery.cs @@ -0,0 +1,5 @@ +using MediatR; + +namespace DbFirst.Application.MassData.Queries; + +public record GetMassDataByCustomerNameQuery(string CustomerName) : IRequest; diff --git a/DbFirst.Application/Repositories/IMassDataRepository.cs b/DbFirst.Application/Repositories/IMassDataRepository.cs new file mode 100644 index 0000000..b578dc9 --- /dev/null +++ b/DbFirst.Application/Repositories/IMassDataRepository.cs @@ -0,0 +1,10 @@ +using DbFirst.Domain.Entities; + +namespace DbFirst.Application.Repositories; + +public interface IMassDataRepository +{ + Task> GetAllAsync(int? skip = null, int? take = null, CancellationToken cancellationToken = default); + Task GetByCustomerNameAsync(string customerName, CancellationToken cancellationToken = default); + Task UpsertByCustomerNameAsync(string customerName, decimal amount, bool statusFlag, string category, CancellationToken cancellationToken = default); +} diff --git a/DbFirst.Domain/Entities/Massdata.cs b/DbFirst.Domain/Entities/Massdata.cs new file mode 100644 index 0000000..03b12e9 --- /dev/null +++ b/DbFirst.Domain/Entities/Massdata.cs @@ -0,0 +1,12 @@ +namespace DbFirst.Domain.Entities; + +public class Massdata +{ + public int Id { get; set; } + public string CustomerName { get; set; } = string.Empty; + public decimal Amount { get; set; } + public string Category { get; set; } = string.Empty; + public bool StatusFlag { get; set; } + public DateTime AddedWhen { get; set; } + public DateTime? ChangedWhen { get; set; } +} diff --git a/DbFirst.Infrastructure/DependencyInjection.cs b/DbFirst.Infrastructure/DependencyInjection.cs index d01436f..4537270 100644 --- a/DbFirst.Infrastructure/DependencyInjection.cs +++ b/DbFirst.Infrastructure/DependencyInjection.cs @@ -11,6 +11,10 @@ public static class DependencyInjection services.Configure(configuration.GetSection("TableConfigurations")); services.AddDbContext(options => options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"))); + + services.AddDbContext(options => + options.UseSqlServer(configuration.GetConnectionString("MassDataConnection"))); + return services; } } diff --git a/DbFirst.Infrastructure/MassDataDbContext.cs b/DbFirst.Infrastructure/MassDataDbContext.cs new file mode 100644 index 0000000..1975663 --- /dev/null +++ b/DbFirst.Infrastructure/MassDataDbContext.cs @@ -0,0 +1,44 @@ +using DbFirst.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace DbFirst.Infrastructure; + +public class MassDataDbContext : DbContext +{ + public MassDataDbContext(DbContextOptions options) + : base(options) + { + } + + public virtual DbSet Massdata { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.ToTable("MASSDATA"); + + entity.Property(e => e.Id).HasColumnName("ID"); + entity.Property(e => e.CustomerName) + .HasMaxLength(200) + .IsUnicode(false) + .HasColumnName("CustomerName"); + entity.Property(e => e.Amount) + .HasColumnType("decimal(12,2)") + .HasColumnName("Amount"); + entity.Property(e => e.Category) + .HasMaxLength(100) + .IsUnicode(false) + .HasColumnName("Category"); + entity.Property(e => e.StatusFlag) + .HasColumnName("StatusFlag"); + entity.Property(e => e.AddedWhen) + .HasColumnType("datetime") + .HasColumnName("ADDED_WHEN"); + entity.Property(e => e.ChangedWhen) + .HasColumnType("datetime") + .HasColumnName("CHANGED_WHEN"); + }); + } +} diff --git a/DbFirst.Infrastructure/Repositories/MassDataRepository.cs b/DbFirst.Infrastructure/Repositories/MassDataRepository.cs new file mode 100644 index 0000000..e054a98 --- /dev/null +++ b/DbFirst.Infrastructure/Repositories/MassDataRepository.cs @@ -0,0 +1,66 @@ +using System.Data; +using DbFirst.Application.Repositories; +using DbFirst.Domain.Entities; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; + +namespace DbFirst.Infrastructure.Repositories; + +public class MassDataRepository : IMassDataRepository +{ + private readonly MassDataDbContext _db; + + public MassDataRepository(MassDataDbContext db) + { + _db = db; + } + + public async Task> GetAllAsync(CancellationToken cancellationToken = default) + { + return await _db.Massdata.AsNoTracking().ToListAsync(cancellationToken); + } + + public async Task GetByCustomerNameAsync(string customerName, CancellationToken cancellationToken = default) + { + return await _db.Massdata.AsNoTracking() + .FirstOrDefaultAsync(x => x.CustomerName == customerName, cancellationToken); + } + + public async Task> GetAllAsync(int? skip = null, int? take = null, CancellationToken cancellationToken = default) + { + var query = _db.Massdata.AsNoTracking().OrderBy(x => x.Id).AsQueryable(); + + if (skip.HasValue) + { + query = query.Skip(skip.Value); + } + + if (take.HasValue) + { + query = query.Take(take.Value); + } + + return await query.ToListAsync(cancellationToken); + } + + public async Task UpsertByCustomerNameAsync(string customerName, decimal amount, bool statusFlag, string category, CancellationToken cancellationToken = default) + { + var customerParam = new SqlParameter("@CustomerName", SqlDbType.VarChar, 200) { Value = customerName }; + var amountParam = new SqlParameter("@Amount", SqlDbType.Decimal) { Value = amount, Precision = 12, Scale = 2 }; + var statusParam = new SqlParameter("@StatusFlag", SqlDbType.Bit) { Value = statusFlag }; + var categoryParam = new SqlParameter("@Category", SqlDbType.VarChar, 100) { Value = category }; + + await _db.Database.ExecuteSqlRawAsync( + "EXEC dbo.PRMassdata_UpsertByCustomerName @CustomerName, @Amount, @StatusFlag, @Category", + parameters: new[] { customerParam, amountParam, statusParam, categoryParam }, + cancellationToken: cancellationToken); + + var updated = await GetByCustomerNameAsync(customerName, cancellationToken); + if (updated == null) + { + throw new InvalidOperationException("Upsert completed but record could not be loaded."); + } + + return updated; + } +}