Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Streetcode/Streetcode.BLL/DTO/Media/Video/CreateVideoDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Streetcode.BLL.DTO.Media.Video
{
public class CreateVideoDTO : VideoDTO
{
}
Comment on lines +3 to +5

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Create DTO must not expose Id — avoid overposting/identity insert issues.

Inheriting from VideoDTO brings Id into the create contract. A client can set it, leading to inserts with explicit identity or unexpected behavior in EF.

Refactor CreateVideoDTO to its own shape without Id:

-namespace Streetcode.BLL.DTO.Media.Video
-{
-    public class CreateVideoDTO : VideoDTO
-    {
-    }
-}
+namespace Streetcode.BLL.DTO.Media.Video
+{
+    public class CreateVideoDTO
+    {
+        public required string Title { get; set; }  // validator enforces max 100
+        public string? Description { get; set; }
+        public required string Url { get; set; }
+        public int StreetcodeId { get; set; }
+    }
+}

If keeping inheritance temporarily, at minimum ensure AutoMapper ignores Id in the create mapping: ForMember(d => d.Id, opt => opt.Ignore());

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public class CreateVideoDTO : VideoDTO
{
}
namespace Streetcode.BLL.DTO.Media.Video
{
public class CreateVideoDTO
{
public required string Title { get; set; } // validator enforces max 100
public string? Description { get; set; }
public required string Url { get; set; }
public int StreetcodeId { get; set; }
}
}
🤖 Prompt for AI Agents
In Streetcode/Streetcode.BLL/DTO/Media/Video/CreateVideoDTO.cs around lines 3-5,
the CreateVideoDTO currently inherits VideoDTO and therefore exposes Id which
can cause overposting/identity insert issues; change CreateVideoDTO to a
standalone DTO type that declares the create-time properties (title, url,
duration, etc.) but omits Id, and update any usages to the new shape; if you
must keep inheritance temporarily, update the AutoMapper profile for the create
mapping to explicitly ignore Id (ForMember(d => d.Id, opt => opt.Ignore())) so
incoming Id values are not mapped.

}
13 changes: 6 additions & 7 deletions Streetcode/Streetcode.BLL/DTO/Media/Video/VideoDTO.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
using Streetcode.BLL.DTO.AdditionalContent;

namespace Streetcode.BLL.DTO.Media.Video;

public class VideoDTO
{
public int Id { get; set; }
public string? Description { get; set; }
public string? Url { get; set; }
public int StreetcodeId { get; set; }
}
public int Id { get; set; }
public string? Title { get; set; }
public string? Description { get; set; }
public string? Url { get; set; }
public int StreetcodeId { get; set; }
}
11 changes: 0 additions & 11 deletions Streetcode/Streetcode.BLL/DTO/Media/VideoDTO.cs

This file was deleted.

10 changes: 10 additions & 0 deletions Streetcode/Streetcode.BLL/DTO/TextContent/TermCreateDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Streetcode.BLL.DTO.Streetcode.TextContent;

public class TermCreateDTO
{
public string Title { get; set; }

public string Description { get; set; }

public int StreetcodeId { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ public class TermProfile : Profile
public TermProfile()
{
CreateMap<Term, TermDTO>().ReverseMap();
CreateMap<TermCreateDTO, Term>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using FluentResults;
using MediatR;
using Streetcode.BLL.DTO.Media.Video;

namespace Streetcode.BLL.MediatR.Media.Video.Create;

public record CreateVideoCommand(CreateVideoDTO CreateVideoRequest) : IRequest<Result<VideoDTO>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using AutoMapper;
using FluentResults;
using MediatR;
using Microsoft.IdentityModel.Tokens;
using Streetcode.BLL.DTO.Media.Video;
using Streetcode.BLL.Interfaces.Logging;
using Streetcode.DAL.Repositories.Interfaces.Base;

using Entity = Streetcode.DAL.Entities.Media.Video;

namespace Streetcode.BLL.MediatR.Media.Video.Create;

public class CreateVideoHandler : IRequestHandler<CreateVideoCommand, Result<VideoDTO>>
{
private readonly IMapper _mapper;
private readonly IRepositoryWrapper _repositoryWrapper;
private readonly ILoggerService _logger;

public CreateVideoHandler(
IRepositoryWrapper repositoryWrapper,
IMapper mapper,
ILoggerService logger)
{
_repositoryWrapper = repositoryWrapper;
_mapper = mapper;
_logger = logger;
}

public async Task<Result<VideoDTO>> Handle(CreateVideoCommand request, CancellationToken cancellationToken)
{
var newVideo = _mapper.Map<Entity>(request.CreateVideoRequest);

var validation = VideoValidation(request, newVideo);
if (validation != null)
{
return validation;
}

var entity = await _repositoryWrapper.VideoRepository.CreateAsync(newVideo);
var resultIsSuccess = await _repositoryWrapper.SaveChangesAsync() > 0;
if (resultIsSuccess)
{
return Result.Ok(_mapper.Map<VideoDTO>(entity));
}
else
{
const string errorMsg = "Failed to create a Video.";
return LogAndFail(request, errorMsg);
}
}

private Result<VideoDTO>? VideoValidation(CreateVideoCommand request, Entity? newVideo)
{
if (newVideo == null)
{
const string errorMsg = "Cannot convert null to Video.";
return LogAndFail(request, errorMsg);
}

if (newVideo.Title.Length > 100)
{
const string errorMsg = "Çàãîëîâîê â³äåî íå ìîæå áóòè á³ëüøå 100 ñèìâîë³â.";
return LogAndFail(request, errorMsg);
}

if (newVideo.Url.IsNullOrEmpty())
{
const string errorMsg = "Ïîñèëàííÿ íà â³äåî º îáîâ'ÿçêîâèì.";
return LogAndFail(request, errorMsg);
}

return null;
}
Comment on lines +52 to +73

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix potential NRE, replace mojibake messages, and use BCL null/whitespace checks

newVideo.Title.Length will throw if Title is null; error texts appear corrupted; and IsNullOrEmpty() extension is unnecessary. Use string.IsNullOrWhiteSpace, check length safely, and provide clear messages.

-    private Result<VideoDTO>? VideoValidation(CreateVideoCommand request, Entity? newVideo)
+    private Result<VideoDTO>? VideoValidation(CreateVideoCommand request, Entity? newVideo)
     {
         if (newVideo == null)
         {
-            const string errorMsg = "Cannot convert null to Video.";
+            const string errorMsg = "Cannot convert null to Video.";
             return LogAndFail(request, errorMsg);
         }
 
-        if (newVideo.Title.Length > 100)
+        if (string.IsNullOrWhiteSpace(newVideo.Title) || newVideo.Title!.Length > 100)
         {
-            const string errorMsg = "��������� ���� �� ���� ���� ����� 100 �������.";
+            const string errorMsg = "Title is required and must not exceed 100 characters.";
             return LogAndFail(request, errorMsg);
         }
 
-        if (newVideo.Url.IsNullOrEmpty())
+        if (string.IsNullOrWhiteSpace(newVideo.Url) || !Uri.IsWellFormedUriString(newVideo.Url, UriKind.Absolute))
         {
-            const string errorMsg = "��������� �� ���� � ����'�������.";
+            const string errorMsg = "URL is required and must be absolute and well-formed.";
             return LogAndFail(request, errorMsg);
         }
 
         return null;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private Result<VideoDTO>? VideoValidation(CreateVideoCommand request, Entity? newVideo)
{
if (newVideo == null)
{
const string errorMsg = "Cannot convert null to Video.";
return LogAndFail(request, errorMsg);
}
if (newVideo.Title.Length > 100)
{
const string errorMsg = "Çàãîëîâîê â³äåî íå ìîæå áóòè á³ëüøå 100 ñèìâîë³â.";
return LogAndFail(request, errorMsg);
}
if (newVideo.Url.IsNullOrEmpty())
{
const string errorMsg = "Ïîñèëàííÿ íà â³äåî º îáîâ'ÿçêîâèì.";
return LogAndFail(request, errorMsg);
}
return null;
}
private Result<VideoDTO>? VideoValidation(CreateVideoCommand request, Entity? newVideo)
{
if (newVideo == null)
{
const string errorMsg = "Cannot convert null to Video.";
return LogAndFail(request, errorMsg);
}
if (string.IsNullOrWhiteSpace(newVideo.Title) || newVideo.Title!.Length > 100)
{
const string errorMsg = "Title is required and must not exceed 100 characters.";
return LogAndFail(request, errorMsg);
}
if (string.IsNullOrWhiteSpace(newVideo.Url) || !Uri.IsWellFormedUriString(newVideo.Url, UriKind.Absolute))
{
const string errorMsg = "URL is required and must be absolute and well-formed.";
return LogAndFail(request, errorMsg);
}
return null;
}
🤖 Prompt for AI Agents
In Streetcode/Streetcode.BLL/MediatR/Media/Video/Create/CreateVideoHandler.cs
around lines 52-73, the validation reads properties without null-safety and
contains corrupted messages; update the method to first null-check newVideo,
then use string.IsNullOrWhiteSpace(newVideo.Title) or safe access (e.g. if
(string.IsNullOrWhiteSpace(newVideo.Title)) ...) before checking length, use
newVideo.Title.Length only after ensuring Title is not null, replace
IsNullOrEmpty() extension with string.IsNullOrWhiteSpace(newVideo.Url), and
replace the mojibake error texts with clear English messages (e.g. "Title is
required", "Title must be at most 100 characters", "URL is required"); keep
returning LogAndFail(request, errorMsg) on failures.


private Result<VideoDTO> LogAndFail(CreateVideoCommand request, string errorMsg)
{
_logger.LogError(request, errorMsg);
return Result.Fail(errorMsg);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using FluentResults;
using MediatR;
using Streetcode.BLL.DTO.Streetcode.TextContent;

namespace Streetcode.BLL.MediatR.Streetcode.Term.Create;

public record CreateTermCommand(TermCreateDTO Term) : IRequest<Result<TermDTO>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using AutoMapper;
using FluentResults;
using MediatR;
using Streetcode.BLL.DTO.Streetcode.TextContent;
using Streetcode.BLL.Interfaces.Logging;
using Streetcode.DAL.Repositories.Interfaces.Base;

using TermEntity = Streetcode.DAL.Entities.Streetcode.TextContent.Term;

namespace Streetcode.BLL.MediatR.Streetcode.Term.Create;

public class CreateTermHandler : IRequestHandler<CreateTermCommand, Result<TermDTO>>
{
private readonly IRepositoryWrapper _repository;
private readonly IMapper _mapper;
private readonly ILoggerService _logger;

public CreateTermHandler(IRepositoryWrapper repository, IMapper mapper, ILoggerService logger)
{
_repository = repository;
_mapper = mapper;
_logger = logger;
}

public async Task<Result<TermDTO>> Handle(CreateTermCommand request, CancellationToken cancellationToken)
{
var entity = _mapper.Map<TermEntity>(request.Term);

if (entity is null)
{
const string errorMsg = "Cannot map CreateTermRequest to entity.";
_logger.LogError(request, errorMsg);
return Result.Fail(errorMsg);
}

await _repository.TermRepository.CreateAsync(entity);
var result = await _repository.SaveChangesAsync();

if (result > 0)
{
var dto = _mapper.Map<TermDTO>(entity);
return Result.Ok(dto);
}

const string failMsg = "Failed to save new Term.";
_logger.LogError(request, failMsg);
return Result.Fail(failMsg);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using FluentValidation;
using Streetcode.BLL.MediatR.Media.Video.Create;

namespace Streetcode.BLL.Validator.Media.Video;

public sealed class CreateVideoCommandValidator : AbstractValidator<CreateVideoCommand>
{
public CreateVideoCommandValidator()
{
RuleFor(c => c.CreateVideoRequest.Title)
.NotEmpty()
.MaximumLength(100);

RuleFor(c => c.CreateVideoRequest.Url)
.NotEmpty()
.Must(url => Uri.IsWellFormedUriString(url, UriKind.Absolute));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using FluentValidation;
using Streetcode.BLL.MediatR.Streetcode.Term.Create;

namespace Streetcode.BLL.Validator.Streetcode.Term.Create;

public sealed class CreateTermValidator : AbstractValidator<CreateTermCommand>
{
public CreateTermValidator()
{
RuleFor(cmd => cmd.Term.Title)
.NotEmpty()
.MaximumLength(200);
Comment on lines +10 to +12

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Confirm DB constraint parity for Title length (200).

Ensure Term.Title in DAL has [MaxLength(200)] (or equivalent) to avoid runtime DB truncation errors.


🏁 Script executed:

#!/bin/bash
# Verify DAL constraint for Term.Title
rg -nP '(class\\s+Term\\b|\\[MaxLength\\(\\s*200\\s*\\)\\])' -C3 --type=cs

Length of output: 25219


🏁 Script executed:

#!/bin/bash
# Locate the Term entity class in DAL and verify Title length annotation
echo "Searching for Term entity definition…"
rg -nP 'public\s+class\s+Term\b' -C3 --type=cs

echo
echo "Checking for [MaxLength(200)] on Title property in Term entity…"
TERM_FILE=$(rg -lP 'public\s+class\s+Term\b' --type=cs)
rg -nP '\[MaxLength\(\s*200\s*\)\]' -C3 "$TERM_FILE"

Length of output: 788


Add DB constraint for Term.Title
The Title property in Streetcode/Streetcode.DAL/Entities/Streetcode/TextContent/Term.cs lacks a [MaxLength(200)] attribute (or equivalent EF Core fluent‐API .HasMaxLength(200)), mismatching the BLL validator’s 200-char limit and risking runtime truncation errors.

🤖 Prompt for AI Agents
In Streetcode/Streetcode.BLL/Validators/TextContent/CreateTermValidator.cs
around lines 10-12, the BLL enforces a 200-char max for Term.Title but the Term
entity lacks a corresponding DB constraint; update
Streetcode/Streetcode.DAL/Entities/Streetcode/TextContent/Term.cs to add
[MaxLength(200)] (or configure .HasMaxLength(200) in the DbContext fluent API)
on the Title property and then add and run an EF Core migration to persist the
schema change so the database matches the validator.


RuleFor(cmd => cmd.Term.Description)
.NotEmpty();

RuleFor(cmd => cmd.Term.StreetcodeId)
.GreaterThan(0);
}
}
2 changes: 1 addition & 1 deletion Streetcode/Streetcode.DAL/Entities/Media/Video.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class Video
public string? Description { get; set; }

[Required]
public string? Url { get; set; }
public string Url { get; set; }

[Required]
public int StreetcodeId { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;

namespace Streetcode.DAL.Entities.Streetcode.TextContent
Expand All @@ -17,4 +17,4 @@ public class RelatedTerm
public int TermId { get; set; }
public Term? Term { get; set; }
}
}
}
38 changes: 22 additions & 16 deletions Streetcode/Streetcode.DAL/Entities/Streetcode/TextContent/Term.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
using System.ComponentModel.DataAnnotations;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Streetcode.DAL.Entities.Streetcode.TextContent;

[Table("terms", Schema = "streetcode")]
public class Term
namespace Streetcode.DAL.Entities.Streetcode.TextContent
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Table("terms", Schema = "streetcode")]
public class Term
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }

[Required]
[MaxLength(50)]
public string Title { get; set; } = string.Empty;

[Required]
[MaxLength(50)]
public string? Title { get; set; }
[Required]
[MaxLength(500)]
public string Description { get; set; } = string.Empty;

[Required]
[MaxLength(500)]
public string? Description { get; set; }
[ForeignKey(nameof(Streetcode))]
public int StreetcodeId { get; set; }

public List<RelatedTerm> RelatedTerms { get; set; } = new();
}
public ICollection<RelatedTerm> RelatedTerms { get; set; } = new List<RelatedTerm>();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using Streetcode.BLL.DTO.Media;
using Streetcode.BLL.DTO.Media.Video;
using Streetcode.BLL.MediatR.Media.Video.Create;
using Streetcode.BLL.MediatR.Media.Video.GetAll;
using Streetcode.BLL.MediatR.Media.Video.GetById;
using Streetcode.BLL.MediatR.Media.Video.GetByStreetcodeId;
Expand All @@ -25,4 +26,10 @@ public async Task<IActionResult> GetById([FromRoute] int id)
{
return HandleResult(await Mediator.Send(new GetVideoByIdQuery(id)));
}
}

[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateVideoDTO video)
{
return HandleResult(await Mediator.Send(new CreateVideoCommand(video)));
}
Comment on lines +30 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

POST Create endpoint: OK; verify existing GET route ambiguity

Create action wiring is fine. However, current GET routes use identical templates "{int}" for both GetByStreetcodeId and GetById, which leads to ambiguous matches at runtime. Consider distinct templates (example below).

Example fix outside this hunk (for lines 18-26):

[HttpGet("by-streetcode/{streetcodeId:int}")]
public async Task<IActionResult> GetByStreetcodeId([FromRoute] int streetcodeId) =>
    HandleResult(await Mediator.Send(new GetVideoByStreetcodeIdQuery(streetcodeId)));

[HttpGet("{id:int}")]
public async Task<IActionResult> GetById([FromRoute] int id) =>
    HandleResult(await Mediator.Send(new GetVideoByIdQuery(id)));

🏁 Script executed:

#!/bin/bash
# Locate the VideoController file
file=$(fd --hidden --full-path VideoController.cs)

# Show all HttpGet attributes with context
rg -nC2 "\[HttpGet" "$file"

# Show GetByStreetcodeId action with context
rg -nC2 "GetByStreetcodeId" "$file"

# Show GetById action with context
rg -nC2 "GetById" "$file"

Length of output: 1277


Differentiate GET route templates to avoid ambiguity
Change GetByStreetcodeId to use a unique template (e.g. [HttpGet("by-streetcode/{streetcodeId:int}")]) instead of "{streetcodeId:int}", so it won’t collide with GetById’s "{id:int}".

🤖 Prompt for AI Agents
In Streetcode/Streetcode.WebApi/Controllers/Media/VideoController.cs around
lines 30 to 34, the GET action that returns videos by streetcodeId uses the same
numeric route template as GetById and can collide; change its route attribute to
a unique template such as [HttpGet("by-streetcode/{streetcodeId:int}")] (and
update any route names or client calls/Swagger references accordingly) so the
two endpoints no longer conflict.

}
1 change: 1 addition & 0 deletions Streetcode/Streetcode.WebApi/Streetcode.WebApi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Streetcode.BLL\Streetcode.BLL.csproj" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0" />
<PackageReference Include="EPPlus" Version="6.1.1" />
<PackageReference Include="FluentResults" Version="3.15.1" />
Expand Down
Loading
Loading