Implement CRUD operations for Brewery, including service and repository layers

This commit is contained in:
Aaron Po
2026-03-29 20:08:21 -04:00
parent 56c83db207
commit 1b467ac4f1
12 changed files with 296 additions and 17 deletions

View File

@@ -0,0 +1,11 @@
using System;
namespace API.Core.Contracts.Breweries;
public class BreweryCreateDto
{
public Guid PostedById { get; set; }
public string BreweryName { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public BreweryLocationCreateDto Location { get; set; } = new BreweryLocationCreateDto();
}

View File

@@ -0,0 +1,15 @@
using System;
namespace API.Core.Contracts.Breweries;
public class BreweryDto
{
public Guid BreweryPostId { get; set; }
public Guid PostedById { get; set; }
public string BreweryName { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public byte[]? Timer { get; set; }
public BreweryLocationDto? Location { get; set; }
}

View File

@@ -0,0 +1,12 @@
using System;
namespace API.Core.Contracts.Breweries;
public class BreweryLocationCreateDto
{
public Guid CityId { get; set; }
public string AddressLine1 { get; set; } = string.Empty;
public string? AddressLine2 { get; set; }
public string PostalCode { get; set; } = string.Empty;
public byte[]? Coordinates { get; set; }
}

View File

@@ -0,0 +1,14 @@
using System;
namespace API.Core.Contracts.Breweries;
public class BreweryLocationDto
{
public Guid BreweryPostLocationId { get; set; }
public Guid BreweryPostId { get; set; }
public Guid CityId { get; set; }
public string AddressLine1 { get; set; } = string.Empty;
public string? AddressLine2 { get; set; }
public string PostalCode { get; set; } = string.Empty;
public byte[]? Coordinates { get; set; }
}

View File

@@ -4,24 +4,10 @@ using Infrastructure.Repository.Sql;
namespace Infrastructure.Repository.Breweries;
public interface IBreweryRepository
public class BreweryRepository(ISqlConnectionFactory connectionFactory)
: Repository<BreweryPost>(connectionFactory), IBreweryRepository
{
Task<BreweryPost?> GetByIdAsync(Guid id);
Task<IEnumerable<BreweryPost>> GetAllAsync(int? limit, int? offset);
Task UpdateAsync(BreweryPost brewery);
Task DeleteAsync(Guid id);
Task CreateAsync(BreweryPost brewery);
}
public class BreweryRepository : Repository<BreweryPost>, IBreweryRepository
{
private readonly ISqlConnectionFactory _connectionFactory;
public BreweryRepository(ISqlConnectionFactory connectionFactory)
: base(connectionFactory)
{
_connectionFactory = connectionFactory;
}
private readonly ISqlConnectionFactory _connectionFactory = connectionFactory;
public async Task<BreweryPost?> GetByIdAsync(Guid id)
{

View File

@@ -0,0 +1,12 @@
using Domain.Entities;
namespace Infrastructure.Repository.Breweries;
public interface IBreweryRepository
{
Task<BreweryPost?> GetByIdAsync(Guid id);
Task<IEnumerable<BreweryPost>> GetAllAsync(int? limit, int? offset);
Task UpdateAsync(BreweryPost brewery);
Task DeleteAsync(Guid id);
Task CreateAsync(BreweryPost brewery);
}

View File

@@ -0,0 +1,69 @@
using FluentAssertions;
using Xunit;
using Service.Breweries;
using API.Core.Contracts.Breweries;
using Domain.Entities;
namespace Service.Breweries.Tests;
public class BreweryServiceTests
{
private class FakeRepo : IBreweryRepository
{
public BreweryPost? Created;
public Task<BreweryPost?> GetByIdAsync(Guid id) => Task.FromResult<BreweryPost?>(null);
public Task<IEnumerable<BreweryPost>> GetAllAsync(int? limit, int? offset) => Task.FromResult<IEnumerable<BreweryPost>>(Array.Empty<BreweryPost>());
public Task UpdateAsync(BreweryPost brewery) { Created = brewery; return Task.CompletedTask; }
public Task DeleteAsync(Guid id) => Task.CompletedTask;
public Task CreateAsync(BreweryPost brewery) { Created = brewery; return Task.CompletedTask; }
}
[Fact]
public async Task CreateAsync_ReturnsFailure_WhenLocationMissing()
{
var repo = new FakeRepo();
var svc = new BreweryService(repo);
var dto = new BreweryCreateDto
{
PostedById = Guid.NewGuid(),
BreweryName = "X",
Description = "Y",
Location = null!
};
var result = await svc.CreateAsync(dto);
result.Success.Should().BeFalse();
result.Message.Should().Contain("Location");
}
[Fact]
public async Task CreateAsync_ReturnsSuccess_AndPersistsEntity()
{
var repo = new FakeRepo();
var svc = new BreweryService(repo);
var loc = new BreweryLocationCreateDto
{
CityId = Guid.NewGuid(),
AddressLine1 = "123 Main",
PostalCode = "12345"
};
var dto = new BreweryCreateDto
{
PostedById = Guid.NewGuid(),
BreweryName = "MyBrew",
Description = "Desc",
Location = loc
};
var result = await svc.CreateAsync(dto);
result.Success.Should().BeTrue();
repo.Created.Should().NotBeNull();
repo.Created!.BreweryName.Should().Be("MyBrew");
result.Brewery.BreweryName.Should().Be("MyBrew");
}
}

View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<RootNamespace>Service.Breweries.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="FluentAssertions" Version="6.9.0" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Service.Breweries\Service.Breweries.csproj" />
<ProjectReference Include="..\Service.Auth\Service.Auth.csproj" />
<ProjectReference
Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
<ProjectReference Include="..\..\API\API.Core\API.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,72 @@
using Domain.Entities;
using Infrastructure.Repository.Breweries;
using API.Core.Contracts.Breweries;
namespace Service.Breweries;
public class BreweryService(IBreweryRepository repository) : IBreweryService
{
private readonly IBreweryRepository _repository = repository;
public Task<BreweryPost?> GetByIdAsync(Guid id) => _repository.GetByIdAsync(id);
public Task<IEnumerable<BreweryPost>> GetAllAsync(int? limit = null, int? offset = null) => _repository.GetAllAsync(limit, offset);
public async Task<BreweryServiceReturn> CreateAsync(BreweryCreateDto brewery)
{
if (brewery.Location is null)
return new BreweryServiceReturn("Location must be provided");
var entity = new BreweryPost
{
BreweryPostId = Guid.NewGuid(),
PostedById = brewery.PostedById,
BreweryName = brewery.BreweryName,
Description = brewery.Description,
CreatedAt = DateTime.UtcNow,
Location = new BreweryPostLocation
{
BreweryPostLocationId = Guid.NewGuid(),
CityId = brewery.Location.CityId,
AddressLine1 = brewery.Location.AddressLine1,
AddressLine2 = brewery.Location.AddressLine2,
PostalCode = brewery.Location.PostalCode,
Coordinates = brewery.Location.Coordinates
}
};
await _repository.CreateAsync(entity);
return new BreweryServiceReturn(entity);
}
public async Task<BreweryServiceReturn> UpdateAsync(BreweryDto brewery)
{
if (brewery is null) return new BreweryServiceReturn("Brewery payload is null");
var entity = new BreweryPost
{
BreweryPostId = brewery.BreweryPostId,
PostedById = brewery.PostedById,
BreweryName = brewery.BreweryName,
Description = brewery.Description,
CreatedAt = brewery.CreatedAt,
UpdatedAt = brewery.UpdatedAt,
Timer = brewery.Timer,
Location = brewery.Location is null ? null : new BreweryPostLocation
{
BreweryPostLocationId = brewery.Location.BreweryPostLocationId,
BreweryPostId = brewery.BreweryPostId,
CityId = brewery.Location.CityId,
AddressLine1 = brewery.Location.AddressLine1,
AddressLine2 = brewery.Location.AddressLine2,
PostalCode = brewery.Location.PostalCode,
Coordinates = brewery.Location.Coordinates
}
};
await _repository.UpdateAsync(entity);
return new BreweryServiceReturn(entity);
}
public Task DeleteAsync(Guid id) => _repository.DeleteAsync(id);
}

View File

@@ -0,0 +1,12 @@
using Microsoft.Extensions.DependencyInjection;
namespace Service.Breweries.DependencyInjection;
public static class BreweryServiceCollectionExtensions
{
public static IServiceCollection AddBreweryServices(this IServiceCollection services)
{
services.AddScoped<IBreweryService, BreweryService>();
return services;
}
}

View File

@@ -0,0 +1,33 @@
using API.Core.Contracts.Breweries;
using Domain.Entities;
namespace Service.Breweries;
public interface IBreweryService
{
Task<BreweryPost?> GetByIdAsync(Guid id);
Task<IEnumerable<BreweryPost>> GetAllAsync(int? limit = null, int? offset = null);
Task<BreweryServiceReturn> CreateAsync(BreweryCreateDto brewery);
Task<BreweryServiceReturn> UpdateAsync(BreweryDto brewery);
Task DeleteAsync(Guid id);
}
public record BreweryServiceReturn
{
public bool Success { get; init; } = false;
public BreweryPost Brewery { get; init; }
public string Message { get; init; } = string.Empty;
public BreweryServiceReturn(BreweryPost brewery)
{
Success = true;
Brewery = brewery;
}
public BreweryServiceReturn(string message)
{
Success = false;
Brewery = default!;
Message = message;
}
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
<ProjectReference
Include="..\..\Infrastructure\Infrastructure.Repository\Infrastructure.Repository.csproj" />
<ProjectReference Include="..\..\API\API.Core\API.Core.csproj" />
<ProjectReference Include="..\Service.Auth\Service.Auth.csproj" />
</ItemGroup>
</Project>