Refactor to introduce nullable types and reduce compiler warnings (#801)

* Introduce nullable types to Core, Infrastructure and PublicApi projects

* Refactor unit tests

* Fixed failing tests

* Introduce Parameter object for CatalogItem update method

* Refactor CataloItemDetails param object

* Refactor following PR comments
This commit is contained in:
Philippe Vaillancourt
2022-10-04 16:57:40 +01:00
committed by GitHub
parent aa6305eab1
commit a72dd775ee
51 changed files with 168 additions and 256 deletions

View File

@@ -3,10 +3,12 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>Microsoft.eShopWeb.ApplicationCore</RootNamespace>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Ardalis.GuardClauses" Version="4.0.1" />
<PackageReference Include="Ardalis.Result" Version="4.1.0" />
<PackageReference Include="Ardalis.Specification" Version="6.1.0" />
<PackageReference Include="MediatR" Version="10.0.1" />
<PackageReference Include="System.Security.Claims" Version="4.3.0" />

View File

@@ -2,5 +2,5 @@
public class CatalogSettings
{
public string CatalogBaseUrl { get; set; }
public string? CatalogBaseUrl { get; set; }
}

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using Ardalis.GuardClauses;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
namespace Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate;
@@ -25,7 +26,7 @@ public class Basket : BaseEntity, IAggregateRoot
_items.Add(new BasketItem(catalogItemId, quantity, unitPrice));
return;
}
var existingItem = Items.FirstOrDefault(i => i.CatalogItemId == catalogItemId);
var existingItem = Items.First(i => i.CatalogItemId == catalogItemId);
existingItem.AddQuantity(quantity);
}

View File

@@ -12,10 +12,8 @@ public class Buyer : BaseEntity, IAggregateRoot
public IEnumerable<PaymentMethod> PaymentMethods => _paymentMethods.AsReadOnly();
private Buyer()
{
// required by EF
}
#pragma warning disable CS8618 // Required by Entity Framework
private Buyer() { }
public Buyer(string identity) : this()
{

View File

@@ -2,7 +2,7 @@
public class PaymentMethod : BaseEntity
{
public string Alias { get; private set; }
public string CardId { get; private set; } // actual card data must be stored in a PCI compliant system, like Stripe
public string Last4 { get; private set; }
public string? Alias { get; private set; }
public string? CardId { get; private set; } // actual card data must be stored in a PCI compliant system, like Stripe
public string? Last4 { get; private set; }
}

View File

@@ -11,9 +11,9 @@ public class CatalogItem : BaseEntity, IAggregateRoot
public decimal Price { get; private set; }
public string PictureUri { get; private set; }
public int CatalogTypeId { get; private set; }
public CatalogType CatalogType { get; private set; }
public CatalogType? CatalogType { get; private set; }
public int CatalogBrandId { get; private set; }
public CatalogBrand CatalogBrand { get; private set; }
public CatalogBrand? CatalogBrand { get; private set; }
public CatalogItem(int catalogTypeId,
int catalogBrandId,
@@ -30,15 +30,15 @@ public class CatalogItem : BaseEntity, IAggregateRoot
PictureUri = pictureUri;
}
public void UpdateDetails(string name, string description, decimal price)
public void UpdateDetails(CatalogItemDetails details)
{
Guard.Against.NullOrEmpty(name, nameof(name));
Guard.Against.NullOrEmpty(description, nameof(description));
Guard.Against.NegativeOrZero(price, nameof(price));
Guard.Against.NullOrEmpty(details.Name, nameof(details.Name));
Guard.Against.NullOrEmpty(details.Description, nameof(details.Description));
Guard.Against.NegativeOrZero(details.Price, nameof(details.Price));
Name = name;
Description = description;
Price = price;
Name = details.Name;
Description = details.Description;
Price = details.Price;
}
public void UpdateBrand(int catalogBrandId)
@@ -62,4 +62,18 @@ public class CatalogItem : BaseEntity, IAggregateRoot
}
PictureUri = $"images\\products\\{pictureName}?{new DateTime().Ticks}";
}
public readonly record struct CatalogItemDetails
{
public string? Name { get; }
public string? Description { get; }
public decimal Price { get; }
public CatalogItemDetails(string? name, string? description, decimal price)
{
Name = name;
Description = description;
Price = price;
}
}
}

View File

@@ -12,6 +12,7 @@ public class Address // ValueObject
public string ZipCode { get; private set; }
#pragma warning disable CS8618 // Required by Entity Framework
private Address() { }
public Address(string street, string city, string state, string country, string zipcode)

View File

@@ -19,10 +19,8 @@ public class CatalogItemOrdered // ValueObject
PictureUri = pictureUri;
}
private CatalogItemOrdered()
{
// required by EF
}
#pragma warning disable CS8618 // Required by Entity Framework
private CatalogItemOrdered() {}
public int CatalogItemId { get; private set; }
public string ProductName { get; private set; }

View File

@@ -7,16 +7,12 @@ namespace Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate;
public class Order : BaseEntity, IAggregateRoot
{
private Order()
{
// required by EF
}
#pragma warning disable CS8618 // Required by Entity Framework
private Order() {}
public Order(string buyerId, Address shipToAddress, List<OrderItem> items)
{
Guard.Against.NullOrEmpty(buyerId, nameof(buyerId));
Guard.Against.Null(shipToAddress, nameof(shipToAddress));
Guard.Against.Null(items, nameof(items));
BuyerId = buyerId;
ShipToAddress = shipToAddress;

View File

@@ -6,10 +6,8 @@ public class OrderItem : BaseEntity
public decimal UnitPrice { get; private set; }
public int Units { get; private set; }
private OrderItem()
{
// required by EF
}
#pragma warning disable CS8618 // Required by Entity Framework
private OrderItem() {}
public OrderItem(CatalogItemOrdered itemOrdered, decimal unitPrice, int units)
{

View File

@@ -7,12 +7,6 @@ namespace Ardalis.GuardClauses;
public static class BasketGuards
{
public static void NullBasket(this IGuardClause guardClause, int basketId, Basket basket)
{
if (basket == null)
throw new BasketNotFoundException(basketId);
}
public static void EmptyBasketOnCheckout(this IGuardClause guardClause, IReadOnlyCollection<BasketItem> basketItems)
{
if (!basketItems.Any())

View File

@@ -9,7 +9,7 @@ public static class JsonExtensions
PropertyNameCaseInsensitive = true
};
public static T FromJson<T>(this string json) =>
public static T? FromJson<T>(this string json) =>
JsonSerializer.Deserialize<T>(json, _jsonOptions);
public static string ToJson<T>(this T obj) =>

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Ardalis.Result;
using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate;
namespace Microsoft.eShopWeb.ApplicationCore.Interfaces;
@@ -8,6 +9,6 @@ public interface IBasketService
{
Task TransferBasketAsync(string anonymousId, string userName);
Task<Basket> AddItemToBasket(string username, int catalogItemId, decimal price, int quantity = 1);
Task<Basket> SetQuantities(int basketId, Dictionary<string, int> quantities);
Task<Result<Basket>> SetQuantities(int basketId, Dictionary<string, int> quantities);
Task DeleteBasketAsync(int basketId);
}

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Ardalis.GuardClauses;
using Ardalis.Result;
using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.ApplicationCore.Specifications;
@@ -22,7 +23,7 @@ public class BasketService : IBasketService
public async Task<Basket> AddItemToBasket(string username, int catalogItemId, decimal price, int quantity = 1)
{
var basketSpec = new BasketWithItemsSpecification(username);
var basket = await _basketRepository.GetBySpecAsync(basketSpec);
var basket = await _basketRepository.FirstOrDefaultAsync(basketSpec);
if (basket == null)
{
@@ -39,15 +40,15 @@ public class BasketService : IBasketService
public async Task DeleteBasketAsync(int basketId)
{
var basket = await _basketRepository.GetByIdAsync(basketId);
Guard.Against.Null(basket, nameof(basket));
await _basketRepository.DeleteAsync(basket);
}
public async Task<Basket> SetQuantities(int basketId, Dictionary<string, int> quantities)
public async Task<Result<Basket>> SetQuantities(int basketId, Dictionary<string, int> quantities)
{
Guard.Against.Null(quantities, nameof(quantities));
var basketSpec = new BasketWithItemsSpecification(basketId);
var basket = await _basketRepository.GetBySpecAsync(basketSpec);
Guard.Against.NullBasket(basketId, basket);
var basket = await _basketRepository.FirstOrDefaultAsync(basketSpec);
if (basket == null) return Result<Basket>.NotFound();
foreach (var item in basket.Items)
{
@@ -64,13 +65,11 @@ public class BasketService : IBasketService
public async Task TransferBasketAsync(string anonymousId, string userName)
{
Guard.Against.NullOrEmpty(anonymousId, nameof(anonymousId));
Guard.Against.NullOrEmpty(userName, nameof(userName));
var anonymousBasketSpec = new BasketWithItemsSpecification(anonymousId);
var anonymousBasket = await _basketRepository.GetBySpecAsync(anonymousBasketSpec);
var anonymousBasket = await _basketRepository.FirstOrDefaultAsync(anonymousBasketSpec);
if (anonymousBasket == null) return;
var userBasketSpec = new BasketWithItemsSpecification(userName);
var userBasket = await _basketRepository.GetBySpecAsync(userBasketSpec);
var userBasket = await _basketRepository.FirstOrDefaultAsync(userBasketSpec);
if (userBasket == null)
{
userBasket = new Basket(userName);

View File

@@ -30,9 +30,9 @@ public class OrderService : IOrderService
public async Task CreateOrderAsync(int basketId, Address shippingAddress)
{
var basketSpec = new BasketWithItemsSpecification(basketId);
var basket = await _basketRepository.GetBySpecAsync(basketSpec);
var basket = await _basketRepository.FirstOrDefaultAsync(basketSpec);
Guard.Against.NullBasket(basketId, basket);
Guard.Against.Null(basket, nameof(basket));
Guard.Against.EmptyBasketOnCheckout(basket.Items);
var catalogItemsSpecification = new CatalogItemsSpecification(basket.Items.Select(item => item.CatalogItemId).ToArray());