Compare commits

..

1 commit

16 changed files with 510 additions and 0 deletions

View file

@ -6,8 +6,26 @@ namespace Wishlist.Data;
public class AppDataContext : IdentityDbContext<User>
{
public DbSet<Entities.Wishlist> Wishlists { get; set; }
public DbSet<Wish> Wishes { get; set; }
public AppDataContext() { }
public AppDataContext(DbContextOptions<AppDataContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Entities.Wishlist>()
.HasMany(e => e.Wishes)
.WithOne(e => e.Wishlist)
.HasForeignKey(e => e.WishlistId)
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
builder.Entity<User>()
.HasMany(e => e.Wishlists)
.WithMany(e => e.Owners)
.UsingEntity(j => j.ToTable("OwnerWishlists"));
}
}

View file

@ -1,3 +1,5 @@
using Wishlist.Data.Entities;
namespace Wishlist.Data;
public class Constants
@ -7,4 +9,5 @@ public class Constants
public const string MemberRole = "Member";
// Static Readonly (Runtime const)
public static readonly string[] Roles = [AdminRole, MemberRole];
public static readonly CurrencyInfo DefaultCurrency = CurrencyInfo.DKK;
}

View file

@ -0,0 +1,38 @@
namespace Wishlist.Data.Entities;
public struct CurrencyInfo
{
public CurrencyInfo()
{
CurrencyCode = string.Empty;
CurrencyName = string.Empty;
Symbol = string.Empty;
}
// ReSharper disable InconsistentNaming
public static readonly CurrencyInfo DKK = new()
{
CurrencyCode = "DKK",
CurrencyName = "Danish Crowns",
Symbol = "Kr",
};
public static readonly CurrencyInfo EURO = new()
{
CurrencyCode = "EURO",
CurrencyName = "Euros",
Symbol = "€"
};
public static readonly CurrencyInfo USD = new()
{
CurrencyCode = "USD",
CurrencyName = "US Dollars",
Symbol = "$"
};
// ReSharper restore InconsistentNaming
public string CurrencyCode { get; set; }
public string CurrencyName { get; set; }
public string Symbol { get; set; }
}

View file

@ -0,0 +1,24 @@
namespace Wishlist.Data.Entities;
public struct PriceInfo
{
public double Price { get; set; }
public CurrencyInfo CurrencyInfo { get; set; }
public PriceInfo()
{
Price = 0;
CurrencyInfo = new CurrencyInfo();
}
public PriceInfo(double price)
{
Price = price;
CurrencyInfo = new CurrencyInfo();
}
public PriceInfo(double price, CurrencyInfo currencyInfo)
{
Price = price;
CurrencyInfo = currencyInfo;
}
}

View file

@ -0,0 +1,23 @@
namespace Wishlist.Data.Entities;
public struct ReserveStatus
{
public User? ReserveUser { get; set; }
public static readonly ReserveStatus NotReserve = new ();
public ReserveStatus()
{
ReserveUser = null;
}
public ReserveStatus(User? reserveUser)
{
ReserveUser = reserveUser;
}
public bool IsReserved()
{
return ReserveUser is not null;
}
}

View file

@ -4,5 +4,6 @@ namespace Wishlist.Data.Entities;
public class User : IdentityUser
{
public List<Wishlist> Wishlists { get; set; }
}

View file

@ -0,0 +1,31 @@
namespace Wishlist.Data.Entities;
public struct VisibilityStatus
{
public VisibilityStatus() { }
public List<User> VisibleTo { get; set; } = [];
public bool Published { get; set; }
public bool Public { get; set; }
public bool IsVisibleForUSer(User user)
{
return Public || Published && VisibleTo.Contains(user);
}
public void AddUser(User user)
{
VisibleTo.Add(user);
}
public void RemoveUser(User user)
{
VisibleTo.Remove(user);
}
public bool IsEmpty()
{
return VisibleTo.Count == 0;
}
}

View file

@ -0,0 +1,35 @@
using System.ComponentModel.DataAnnotations;
namespace Wishlist.Data.Entities;
public class Wish
{
public Guid Id { get; set; }
[MaxLength(50)]
public string Name { get; set; }
[MaxLength(2000)]
public string Description { get; set; }
public DateTime Created { get; set; }
public DateTime LastModified { get; set; }
public bool IsVisible { get; set; }
public ReserveStatus ReserveStatus { get; set; }
public PriceInfo Price { get; set; }
public Uri ImageUrl { get; set; }
public Uri StoreLink { get; set; }
public Guid WishlistId { get; set; }
public Wishlist Wishlist { get; set; } = null!;
public Wish()
{
Id = Guid.NewGuid();
Created = DateTime.UtcNow;
LastModified = DateTime.UtcNow;
IsVisible = false;
ReserveStatus = ReserveStatus.NotReserve;
Price = new(0, Constants.DefaultCurrency);
ImageUrl = new Uri(string.Empty);
StoreLink = new Uri(string.Empty);
Description = string.Empty;
Name = string.Empty;
}
}

View file

@ -0,0 +1,28 @@
using System.ComponentModel.DataAnnotations;
namespace Wishlist.Data.Entities;
public class Wishlist
{
public Guid Id { get; set; }
[MaxLength(50)]
public string Title { get; set; }
[MaxLength(2000)]
public string Description { get; set; }
public List<Wish> Wishes { get; set; }
public List<User> Owners { get; set; }
public VisibilityStatus VisibilityStatus { get; set; }
public DateTime Created { get; set; }
public DateTime LastModified { get; set; }
public Wishlist()
{
Id = Guid.NewGuid();
Title = string.Empty;
Description = string.Empty;
Wishes = [];
VisibilityStatus = new VisibilityStatus();
Created = DateTime.UtcNow;
LastModified = DateTime.UtcNow;
}
}

View file

@ -0,0 +1,12 @@
using CSharpFunctionalExtensions;
namespace Wishlist.Data.Services.Interfaces;
public interface ICrud<in TKey, TEntity>
{
Task<Result<TEntity>> Create(TEntity entity);
Task<Maybe<TEntity>> Read(TKey entityId);
Task<Result<TEntity>> Update(TEntity entity);
Task<Result<TEntity>> Delete(TKey entityId);
}

View file

@ -0,0 +1,17 @@
using CSharpFunctionalExtensions;
using Wishlist.Data.Entities;
namespace Wishlist.Data.Services.Interfaces;
public interface IWishService : ICrud<Guid, Wish>
{
Task<Result<List<Wish>>> GetAllWishes(Guid listId);
Task<Result<Wish>> UpdateReservedStatus(Guid wishId, User? reservedBy);
Task<Result<Wish>> UpdatePriceInfo(Guid wishId, PriceInfo priceInfo);
Task<Result<Wish>> UpdateImage(Guid wishId, Uri imageUri);
Task<Result<Wish>> UpdateStore(Guid wishId, Uri storeUri);
Task<Result<Wish>> UpdateName(Guid wishId, string name);
Task<Result<Wish>> UpdateDescription(Guid wishId, string description);
Task<Result<Wish>> UpdateVisibility(Guid wishId, bool isVisible);
Task<Result<Wish>> UpdatePrice(Guid wishId, double price);
}

View file

@ -0,0 +1,9 @@
using CSharpFunctionalExtensions;
using Wishlist.Data.Entities;
namespace Wishlist.Data.Services.Interfaces;
public interface IWishlistService : ICrud<Guid, Entities.Wishlist>
{
Task<Result<Wish>> AddWish(Guid id, Wish wish);
}

View file

@ -0,0 +1,176 @@
using CSharpFunctionalExtensions;
using Wishlist.Data.Entities;
using Wishlist.Data.Services.Interfaces;
namespace Wishlist.Data.Services;
public class WishService : IWishService
{
private readonly AppDataContext _context;
private readonly ILogger<WishService> _logger;
private readonly IWishlistService _wishlistService;
public WishService(AppDataContext context, ILogger<WishService> logger, IWishlistService wishlistService)
{
_context = context;
_logger = logger;
_wishlistService = wishlistService;
}
public async Task<Result<Wish>> Create(Wish entity)
{
try
{
_logger.LogInformation($"Creating Wish {entity.Id}");
entity.Created = DateTime.UtcNow;
entity.LastModified = entity.Created;
var createdEntity = await _context.Wishes.AddAsync(entity);
await _context.SaveChangesAsync();
return createdEntity.Entity;
}
catch (Exception e)
{
_logger.LogError("An error occurred while creating the Wish. {Entity} - {Error}", entity, e.Message);
return Result.Failure<Wish>($"An error occurred while creating the Wish. {e}");
}
}
public async Task<Maybe<Wish>> Read(Guid entityId)
{
var entity = await _context.Wishes.FindAsync(entityId);
return entity;
}
public async Task<Result<Wish>> Update(Wish entity)
{
var oldEntity = await Read(entity.Id);
if (oldEntity.HasNoValue)
{
return Result.Failure<Wish>("The given entity was not found.");
}
entity.LastModified = DateTime.UtcNow;
_context.Wishes.Update(entity);
await _context.SaveChangesAsync();
return Result.Success(entity);
}
public async Task<Result<Wish>> Delete(Guid entityId)
{
var entity = await Read(entityId);
if (entity.HasNoValue)
{
return Result.Failure<Wish>("The given entity was not found.");
}
var deletedEntity = _context.Wishes.Remove(entity.Value);
await _context.SaveChangesAsync();
return Result.Success(deletedEntity.Entity);
}
public async Task<Result<List<Wish>>> GetAllWishes(Guid listId)
{
var listMaybe = await _wishlistService.Read(listId);
if (listMaybe.HasNoValue)
{
return Result.Failure<List<Wish>>("The given list was not found.");
}
return listMaybe.Value.Wishes;
}
public async Task<Result<Wish>> UpdateReservedStatus(Guid wishId, User? reservedBy)
{
var oldEntity = await Read(wishId);
if (oldEntity.HasNoValue)
{
return Result.Failure<Wish>("The given entity was not found.");
}
var wish = oldEntity.Value;
wish.ReserveStatus = new ReserveStatus(reservedBy);
return await Update(wish);
}
public async Task<Result<Wish>> UpdatePriceInfo(Guid wishId, PriceInfo priceInfo)
{
var oldEntity = await Read(wishId);
if (oldEntity.HasNoValue)
{
return Result.Failure<Wish>("The given entity was not found.");
}
var wish = oldEntity.Value;
wish.Price = priceInfo;
return await Update(wish);
}
public async Task<Result<Wish>> UpdateImage(Guid wishId, Uri imageUri)
{
var oldEntity = await Read(wishId);
if (oldEntity.HasNoValue)
{
return Result.Failure<Wish>("The given entity was not found.");
}
var wish = oldEntity.Value;
wish.ImageUrl = imageUri;
return await Update(wish);
}
public async Task<Result<Wish>> UpdateStore(Guid wishId, Uri storeUri)
{
var oldEntity = await Read(wishId);
if (oldEntity.HasNoValue)
{
return Result.Failure<Wish>("The given entity was not found.");
}
var wish = oldEntity.Value;
wish.StoreLink = storeUri;
return await Update(wish);
}
public async Task<Result<Wish>> UpdateName(Guid wishId, string name)
{
var oldEntity = await Read(wishId);
if (oldEntity.HasNoValue)
{
return Result.Failure<Wish>("The given entity was not found.");
}
var wish = oldEntity.Value;
wish.Name = name;
return await Update(wish);
}
public async Task<Result<Wish>> UpdateDescription(Guid wishId, string description)
{
var oldEntity = await Read(wishId);
if (oldEntity.HasNoValue)
{
return Result.Failure<Wish>("The given entity was not found.");
}
var wish = oldEntity.Value;
wish.Description = description;
return await Update(wish);
}
public async Task<Result<Wish>> UpdateVisibility(Guid wishId, bool isVisible)
{
var oldEntity = await Read(wishId);
if (oldEntity.HasNoValue)
{
return Result.Failure<Wish>("The given entity was not found.");
}
var wish = oldEntity.Value;
wish.IsVisible = isVisible;
return await Update(wish);
}
public async Task<Result<Wish>> UpdatePrice(Guid wishId, double price)
{
var oldEntity = await Read(wishId);
if (oldEntity.HasNoValue)
{
return Result.Failure<Wish>("The given entity was not found.");
}
var wish = oldEntity.Value;
var oldPrice = wish.Price;
oldPrice.Price = price;
wish.Price = oldPrice;
return await Update(wish);
}
}

View file

@ -0,0 +1,86 @@
using CSharpFunctionalExtensions;
using Wishlist.Data.Entities;
using Wishlist.Data.Services.Interfaces;
namespace Wishlist.Data.Services;
public class WishlistService : IWishlistService
{
private readonly AppDataContext _context;
private readonly ILogger<WishlistService> _logger;
public WishlistService(AppDataContext context, ILogger<WishlistService> logger)
{
_context = context;
_logger = logger;
}
public async Task<Result<Entities.Wishlist>> Create(Entities.Wishlist entity)
{
try
{
_logger.LogInformation($"Creating Wishlist {entity.Id}");
entity.Created = DateTime.UtcNow;
entity.LastModified = entity.Created;
var createdEntity = await _context.Wishlists.AddAsync(entity);
await _context.SaveChangesAsync();
return createdEntity.Entity;
}
catch (Exception e)
{
_logger.LogError("An error occurred while creating the Wishlist. {Entity} - {Error}", entity, e.Message);
return Result.Failure<Entities.Wishlist>($"An error occurred while creating the Wishlist. {e}");
}
}
public async Task<Maybe<Entities.Wishlist>> Read(Guid entityId)
{
var entity = await _context.Wishlists.FindAsync(entityId);
return entity;
}
public async Task<Result<Entities.Wishlist>> Update(Entities.Wishlist entity)
{
var oldEntity = await Read(entity.Id);
if (oldEntity.HasNoValue)
{
return Result.Failure<Entities.Wishlist>("The given entity was not found.");
}
entity.LastModified = DateTime.UtcNow;
_context.Wishlists.Update(entity);
await _context.SaveChangesAsync();
return Result.Success(entity);
}
public async Task<Result<Entities.Wishlist>> Delete(Guid entityId)
{
var entity = await Read(entityId);
if (entity.HasNoValue)
{
return Result.Failure<Entities.Wishlist>("The given entity was not found.");
}
var deletedEntity = _context.Wishlists.Remove(entity.Value);
await _context.SaveChangesAsync();
return Result.Success(deletedEntity.Entity);
}
public async Task<Result<Wish>> AddWish(Guid id, Wish wish)
{
var listMaybe = await Read(id);
if (listMaybe.HasNoValue)
{
return Result.Failure<Wish>($"Wishlist '{id}' was not found. Wish '{wish.Id}' not added");
}
var wishlist = listMaybe.Value;
wish.WishlistId = wishlist.Id;
wishlist.Wishes.Add(wish);
var update = await Update(wishlist);
if (update.IsFailure)
{
return Result.Failure<Wish>(update.Error);
}
await _context.SaveChangesAsync();
return Result.Success(wish);
}
}

View file

@ -6,6 +6,8 @@ using Microsoft.EntityFrameworkCore;
using Wishlist.Components;
using Wishlist.Data;
using Wishlist.Data.Entities;
using Wishlist.Data.Services;
using Wishlist.Data.Services.Interfaces;
namespace Wishlist;
@ -26,6 +28,8 @@ public class Program
builder.Services.AddScoped<IdentityUserAccessor>();
builder.Services.AddScoped<IdentityRedirectManager>();
builder.Services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();
builder.Services.AddScoped<IWishlistService, WishlistService>();
builder.Services.AddScoped<IWishService, WishService>();
builder.Services.AddAuthentication(options =>
{

View file

@ -9,6 +9,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CSharpFunctionalExtensions" Version="3.7.0" />
<PackageReference Include="CSharpFunctionalExtensions.Analyzers" Version="1.4.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="DotNetEnv" Version="3.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="9.0.12"/>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.12"/>