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());

View File

@@ -8,9 +8,8 @@ namespace Microsoft.eShopWeb.Infrastructure.Data;
public class CatalogContext : DbContext
{
public CatalogContext(DbContextOptions<CatalogContext> options) : base(options)
{
}
#pragma warning disable CS8618 // Required by Entity Framework
public CatalogContext(DbContextOptions<CatalogContext> options) : base(options) {}
public DbSet<Basket> Baskets { get; set; }
public DbSet<CatalogItem> CatalogItems { get; set; }

View File

@@ -9,7 +9,7 @@ public class BasketConfiguration : IEntityTypeConfiguration<Basket>
public void Configure(EntityTypeBuilder<Basket> builder)
{
var navigation = builder.Metadata.FindNavigation(nameof(Basket.Items));
navigation.SetPropertyAccessMode(PropertyAccessMode.Field);
navigation?.SetPropertyAccessMode(PropertyAccessMode.Field);
builder.Property(b => b.BuyerId)
.IsRequired()

View File

@@ -10,7 +10,7 @@ public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
var navigation = builder.Metadata.FindNavigation(nameof(Order.OrderItems));
navigation.SetPropertyAccessMode(PropertyAccessMode.Field);
navigation?.SetPropertyAccessMode(PropertyAccessMode.Field);
builder.Property(b => b.BuyerId)
.IsRequired()

View File

@@ -2,10 +2,10 @@
public class FileItem
{
public string FileName { get; set; }
public string Url { get; set; }
public string? FileName { get; set; }
public string? Url { get; set; }
public long Size { get; set; }
public string Ext { get; set; }
public string Type { get; set; }
public string DataBase64 { get; set; }
public string? Ext { get; set; }
public string? Type { get; set; }
public string? DataBase64 { get; set; }
}

View File

@@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>Microsoft.eShopWeb.Infrastructure</RootNamespace>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>

View File

@@ -42,7 +42,8 @@ public class UpdateCatalogItemEndpoint : IEndpoint<IResult, UpdateCatalogItemReq
var existingItem = await _itemRepository.GetByIdAsync(request.Id);
existingItem.UpdateDetails(request.Name, request.Description, request.Price);
CatalogItem.CatalogItemDetails details = new(request.Name, request.Description, request.Price);
existingItem.UpdateDetails(details);
existingItem.UpdateBrand(request.CatalogBrandId);
existingItem.UpdateType(request.CatalogTypeId);

View File

@@ -6,6 +6,7 @@
<UserSecretsId>5b662463-1efd-4bae-bde4-befe0be3e8ff</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>..\..</DockerfileContext>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>

View File

@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Ardalis.GuardClauses;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
@@ -73,17 +74,17 @@ public class LoginModel : PageModel
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
//var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true);
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, false, true);
var result = await _signInManager.PasswordSignInAsync(Input?.Email, Input?.Password, false, true);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
await TransferAnonymousBasketToUserAsync(Input.Email);
await TransferAnonymousBasketToUserAsync(Input?.Email);
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input?.RememberMe });
}
if (result.IsLockedOut)
{
@@ -108,6 +109,7 @@ public class LoginModel : PageModel
var anonymousId = Request.Cookies[Constants.BASKET_COOKIENAME];
if (Guid.TryParse(anonymousId, out var _))
{
Guard.Against.NullOrEmpty(userName, nameof(userName));
await _basketService.TransferBasketAsync(anonymousId, userName);
}
Response.Cookies.Delete(Constants.BASKET_COOKIENAME);

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Ardalis.GuardClauses;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
@@ -67,8 +68,8 @@ public class RegisterModel : PageModel
returnUrl = returnUrl ?? Url.Content("~/");
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email };
var result = await _userManager.CreateAsync(user, Input.Password);
var user = new ApplicationUser { UserName = Input?.Email, Email = Input?.Email };
var result = await _userManager.CreateAsync(user, Input?.Password);
if (result.Succeeded)
{
_logger.LogInformation("User created a new account with password.");
@@ -80,7 +81,8 @@ public class RegisterModel : PageModel
values: new { userId = user.Id, code = code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
Guard.Against.Null(callbackUrl, nameof(callbackUrl));
await _emailSender.SendEmailAsync(Input?.Email, "Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
await _signInManager.SignInAsync(user, isPersistent: false);

View File

@@ -22,12 +22,12 @@ public class RevokeAuthenticationEvents : CookieAuthenticationEvents
public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
{
var userId = context.Principal.Claims.First(c => c.Type == ClaimTypes.Name);
var userId = context.Principal?.Claims.First(c => c.Type == ClaimTypes.Name);
var identityKey = context.Request.Cookies[ConfigureCookieSettings.IdentifierCookieName];
if (_cache.TryGetValue($"{userId.Value}:{identityKey}", out var revokeKeys))
if (_cache.TryGetValue($"{userId?.Value}:{identityKey}", out var revokeKeys))
{
_logger.LogDebug($"Access has been revoked for: {userId.Value}.");
_logger.LogDebug($"Access has been revoked for: {userId?.Value}.");
context.RejectPrincipal();
await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}

View File

@@ -1,5 +1,6 @@
using System.Text;
using System.Text.Encodings.Web;
using Ardalis.GuardClauses;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
@@ -119,6 +120,7 @@ public class ManageController : Controller
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
Guard.Against.Null(callbackUrl, nameof(callbackUrl));
var email = user.Email;
await _emailSender.SendEmailConfirmationAsync(email, callbackUrl);
@@ -405,7 +407,7 @@ public class ManageController : Controller
}
// Strip spaces and hypens
var verificationCode = model.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
var verificationCode = model.Code?.Replace(" ", string.Empty).Replace("-", string.Empty);
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);

View File

@@ -1,4 +1,5 @@
using MediatR;
using Ardalis.GuardClauses;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.Web.Features.MyOrders;
@@ -21,6 +22,7 @@ public class OrderController : Controller
[HttpGet]
public async Task<IActionResult> MyOrders()
{
Guard.Against.Null(User?.Identity?.Name, nameof(User.Identity.Name));
var viewModel = await _mediator.Send(new GetMyOrders(User.Identity.Name));
return View(viewModel);
@@ -29,6 +31,7 @@ public class OrderController : Controller
[HttpGet("{orderId}")]
public async Task<IActionResult> Detail(int orderId)
{
Guard.Against.Null(User?.Identity?.Name, nameof(User.Identity.Name));
var viewModel = await _mediator.Send(new GetOrderDetails(User.Identity.Name, orderId));
if (viewModel == null)

View File

@@ -28,7 +28,7 @@ public class UserController : ControllerBase
private async Task<UserInfo> CreateUserInfo(ClaimsPrincipal claimsPrincipal)
{
if (!claimsPrincipal.Identity.IsAuthenticated)
if (claimsPrincipal.Identity == null || claimsPrincipal.Identity.Name == null || !claimsPrincipal.Identity.IsAuthenticated)
{
return UserInfo.Anonymous;
}

View File

@@ -2,7 +2,7 @@
public static class UrlHelperExtensions
{
public static string EmailConfirmationLink(this IUrlHelper urlHelper, string userId, string code, string scheme)
public static string? EmailConfirmationLink(this IUrlHelper urlHelper, string userId, string code, string scheme)
{
return urlHelper.Action(
action: "GET",

View File

@@ -28,7 +28,7 @@ public class GetMyOrdersHandler : IRequestHandler<GetMyOrders, IEnumerable<Order
return orders.Select(o => new OrderViewModel
{
OrderDate = o.OrderDate,
OrderItems = o.OrderItems?.Select(oi => new OrderItemViewModel()
OrderItems = o.OrderItems.Select(oi => new OrderItemViewModel()
{
PictureUrl = oi.ItemOrdered.PictureUri,
ProductId = oi.ItemOrdered.CatalogItemId,

View File

@@ -9,7 +9,7 @@ using Microsoft.eShopWeb.Web.ViewModels;
namespace Microsoft.eShopWeb.Web.Features.OrderDetails;
public class GetOrderDetailsHandler : IRequestHandler<GetOrderDetails, OrderViewModel>
public class GetOrderDetailsHandler : IRequestHandler<GetOrderDetails, OrderViewModel?>
{
private readonly IReadRepository<Order> _orderRepository;
@@ -18,11 +18,11 @@ public class GetOrderDetailsHandler : IRequestHandler<GetOrderDetails, OrderView
_orderRepository = orderRepository;
}
public async Task<OrderViewModel> Handle(GetOrderDetails request,
public async Task<OrderViewModel?> Handle(GetOrderDetails request,
CancellationToken cancellationToken)
{
var spec = new OrderWithItemsByIdSpec(request.OrderId);
var order = await _orderRepository.GetBySpecAsync(spec, cancellationToken);
var order = await _orderRepository.FirstOrDefaultAsync(spec, cancellationToken);
if (order == null)
{

View File

@@ -19,8 +19,8 @@ public class HomePageHealthCheck : IHealthCheck
HealthCheckContext context,
CancellationToken cancellationToken = default(CancellationToken))
{
var request = _httpContextAccessor.HttpContext.Request;
string myUrl = request.Scheme + "://" + request.Host.ToString();
var request = _httpContextAccessor.HttpContext?.Request;
string myUrl = request?.Scheme + "://" + request?.Host.ToString();
var client = new HttpClient();
var response = await client.GetAsync(myUrl);

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using Ardalis.GuardClauses;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
@@ -68,6 +69,7 @@ public class CheckoutModel : PageModel
private async Task SetBasketModelAsync()
{
Guard.Against.Null(User?.Identity?.Name, nameof(User.Identity.Name));
if (_signInManager.IsSignedIn(HttpContext.User))
{
BasketModel = await _basketViewModelService.GetOrCreateBasketForUser(User.Identity.Name);
@@ -75,7 +77,7 @@ public class CheckoutModel : PageModel
else
{
GetOrSetBasketCookieAndUserName();
BasketModel = await _basketViewModelService.GetOrCreateBasketForUser(_username);
BasketModel = await _basketViewModelService.GetOrCreateBasketForUser(_username!);
}
}

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using Ardalis.GuardClauses;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
@@ -66,11 +67,13 @@ public class IndexModel : PageModel
private string GetOrSetBasketCookieAndUserName()
{
Guard.Against.Null(Request.HttpContext.User.Identity, nameof(Request.HttpContext.User.Identity));
string? userName = null;
if (Request.HttpContext.User.Identity.IsAuthenticated)
{
return Request.HttpContext.User.Identity.Name;
Guard.Against.Null(Request.HttpContext.User.Identity.Name, nameof(Request.HttpContext.User.Identity.Name));
return Request.HttpContext.User.Identity.Name!;
}
if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME))

View File

@@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using Ardalis.GuardClauses;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.Infrastructure.Identity;
@@ -33,17 +34,18 @@ public class Basket : ViewComponent
{
if (_signInManager.IsSignedIn(HttpContext.User))
{
Guard.Against.Null(User?.Identity?.Name, nameof(User.Identity.Name));
return await _basketService.CountTotalBasketItems(User.Identity.Name);
}
string anonymousId = GetAnnonymousIdFromCookie();
string? anonymousId = GetAnnonymousIdFromCookie();
if (anonymousId == null)
return 0;
return await _basketService.CountTotalBasketItems(anonymousId);
}
private string GetAnnonymousIdFromCookie()
private string? GetAnnonymousIdFromCookie()
{
if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME))
{

View File

@@ -28,7 +28,7 @@ public class BasketViewModelService : IBasketViewModelService
public async Task<BasketViewModel> GetOrCreateBasketForUser(string userName)
{
var basketSpec = new BasketWithItemsSpecification(userName);
var basket = (await _basketRepository.GetBySpecAsync(basketSpec));
var basket = (await _basketRepository.FirstOrDefaultAsync(basketSpec));
if (basket == null)
{

View File

@@ -1,4 +1,4 @@
using System.Threading.Tasks;
using Ardalis.GuardClauses;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.Web.Interfaces;
@@ -18,7 +18,11 @@ public class CatalogItemViewModelService : ICatalogItemViewModelService
public async Task UpdateCatalogItem(CatalogItemViewModel viewModel)
{
var existingCatalogItem = await _catalogItemRepository.GetByIdAsync(viewModel.Id);
existingCatalogItem.UpdateDetails(viewModel.Name, existingCatalogItem.Description, viewModel.Price);
Guard.Against.Null(existingCatalogItem, nameof(existingCatalogItem));
CatalogItem.CatalogItemDetails details = new(viewModel.Name, existingCatalogItem.Description, viewModel.Price);
existingCatalogItem.UpdateDetails(details);
await _catalogItemRepository.UpdateAsync(existingCatalogItem);
}
}

View File

@@ -5,11 +5,13 @@ namespace Microsoft.eShopWeb.Web;
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
public string? TransformOutbound(object value)
public string? TransformOutbound(object? value)
{
if (value == null) { return null; }
string? str = value.ToString();
if (string.IsNullOrEmpty(str)) { return null; }
// Slugify value
return Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2").ToLower();
return Regex.Replace(str, "([a-z])([A-Z])", "$1-$2").ToLower();
}
}

View File

@@ -1,6 +1,4 @@
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.eShopWeb.FunctionalTests.Web;
using Microsoft.eShopWeb.FunctionalTests.Web;
using Xunit;
namespace Microsoft.eShopWeb.FunctionalTests.WebRazorPages;

View File

@@ -49,7 +49,7 @@ public class GetByIdWithItemsAsync
//Act
var spec = new OrderWithItemsByIdSpec(secondOrderId);
var orderFromRepo = await _orderRepository.GetBySpecAsync(spec);
var orderFromRepo = await _orderRepository.FirstOrDefaultAsync(spec);
//Assert
Assert.Equal(secondOrderId, orderFromRepo.Id);

View File

@@ -1,55 +0,0 @@
using System;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Xunit;
namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Entities.CatalogItemTests;
public class UpdateDetails
{
private CatalogItem _testItem;
private int _validTypeId = 1;
private int _validBrandId = 2;
private string _validDescription = "test description";
private string _validName = "test name";
private decimal _validPrice = 1.23m;
private string _validUri = "/123";
public UpdateDetails()
{
_testItem = new CatalogItem(_validTypeId, _validBrandId, _validDescription, _validName, _validPrice, _validUri);
}
[Fact]
public void ThrowsArgumentExceptionGivenEmptyName()
{
string newValue = "";
Assert.Throws<ArgumentException>(() => _testItem.UpdateDetails(newValue, _validDescription, _validPrice));
}
[Fact]
public void ThrowsArgumentExceptionGivenEmptyDescription()
{
string newValue = "";
Assert.Throws<ArgumentException>(() => _testItem.UpdateDetails(_validName, newValue, _validPrice));
}
[Fact]
public void ThrowsArgumentNullExceptionGivenNullName()
{
Assert.Throws<ArgumentNullException>(() => _testItem.UpdateDetails(null, _validDescription, _validPrice));
}
[Fact]
public void ThrowsArgumentNullExceptionGivenNullDescription()
{
Assert.Throws<ArgumentNullException>(() => _testItem.UpdateDetails(_validName, null, _validPrice));
}
[Theory]
[InlineData(0)]
[InlineData(-1.23)]
public void ThrowsArgumentExceptionGivenNonPositivePrice(decimal newPrice)
{
Assert.Throws<ArgumentException>(() => _testItem.UpdateDetails(_validName, _validDescription, newPrice));
}
}

View File

@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Diagnostics.CodeAnalysis;
namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Extensions;
@@ -9,12 +6,22 @@ public class TestParent : IEquatable<TestParent>
{
public int Id { get; set; }
public string Name { get; set; }
public string? Name { get; set; }
public IEnumerable<TestChild> Children { get; set; }
public IEnumerable<TestChild>? Children { get; set; }
public bool Equals([AllowNull] TestParent other) =>
other?.Id == Id && other?.Name == Name &&
(other?.Children is null && Children is null ||
(other?.Children?.Zip(Children)?.All(t => t.First?.Equals(t.Second) ?? false) ?? false));
public bool Equals([AllowNull] TestParent other)
{
if (other?.Id == Id && other?.Name == Name)
{
if (Children is null)
{
return other?.Children is null;
}
return other?.Children?.Zip(Children).All(t => t.First?.Equals(t.Second) ?? false) ?? false;
}
return false;
}
}

View File

@@ -12,19 +12,20 @@ public class AddItemToBasket
{
private readonly string _buyerId = "Test buyerId";
private readonly Mock<IRepository<Basket>> _mockBasketRepo = new();
private readonly Mock<IAppLogger<BasketService>> _mockLogger = new();
[Fact]
public async Task InvokesBasketRepositoryGetBySpecAsyncOnce()
{
var basket = new Basket(_buyerId);
basket.AddItem(1, It.IsAny<decimal>(), It.IsAny<int>());
_mockBasketRepo.Setup(x => x.GetBySpecAsync(It.IsAny<BasketWithItemsSpecification>(), default)).ReturnsAsync(basket);
_mockBasketRepo.Setup(x => x.FirstOrDefaultAsync(It.IsAny<BasketWithItemsSpecification>(), default)).ReturnsAsync(basket);
var basketService = new BasketService(_mockBasketRepo.Object, null);
var basketService = new BasketService(_mockBasketRepo.Object, _mockLogger.Object);
await basketService.AddItemToBasket(basket.BuyerId, 1, 1.50m);
_mockBasketRepo.Verify(x => x.GetBySpecAsync(It.IsAny<BasketWithItemsSpecification>(), default), Times.Once);
_mockBasketRepo.Verify(x => x.FirstOrDefaultAsync(It.IsAny<BasketWithItemsSpecification>(), default), Times.Once);
}
[Fact]
@@ -32,9 +33,9 @@ public class AddItemToBasket
{
var basket = new Basket(_buyerId);
basket.AddItem(1, It.IsAny<decimal>(), It.IsAny<int>());
_mockBasketRepo.Setup(x => x.GetBySpecAsync(It.IsAny<BasketWithItemsSpecification>(), default)).ReturnsAsync(basket);
_mockBasketRepo.Setup(x => x.FirstOrDefaultAsync(It.IsAny<BasketWithItemsSpecification>(), default)).ReturnsAsync(basket);
var basketService = new BasketService(_mockBasketRepo.Object, null);
var basketService = new BasketService(_mockBasketRepo.Object, _mockLogger.Object);
await basketService.AddItemToBasket(basket.BuyerId, 1, 1.50m);

View File

@@ -11,6 +11,7 @@ public class DeleteBasket
{
private readonly string _buyerId = "Test buyerId";
private readonly Mock<IRepository<Basket>> _mockBasketRepo = new();
private readonly Mock<IAppLogger<BasketService>> _mockLogger = new();
[Fact]
public async Task ShouldInvokeBasketRepositoryDeleteAsyncOnce()
@@ -20,7 +21,7 @@ public class DeleteBasket
basket.AddItem(2, It.IsAny<decimal>(), It.IsAny<int>());
_mockBasketRepo.Setup(x => x.GetByIdAsync(It.IsAny<int>(), default))
.ReturnsAsync(basket);
var basketService = new BasketService(_mockBasketRepo.Object, null);
var basketService = new BasketService(_mockBasketRepo.Object, _mockLogger.Object);
await basketService.DeleteBasketAsync(It.IsAny<int>());

View File

@@ -1,34 +0,0 @@
using System;
using System.Threading.Tasks;
using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate;
using Microsoft.eShopWeb.ApplicationCore.Exceptions;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.ApplicationCore.Services;
using Moq;
using Xunit;
namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Services.BasketServiceTests;
public class SetQuantities
{
private readonly int _invalidId = -1;
private readonly Mock<IRepository<Basket>> _mockBasketRepo = new();
[Fact]
public async Task ThrowsGivenInvalidBasketId()
{
var basketService = new BasketService(_mockBasketRepo.Object, null);
await Assert.ThrowsAsync<BasketNotFoundException>(async () =>
await basketService.SetQuantities(_invalidId, new System.Collections.Generic.Dictionary<string, int>()));
}
[Fact]
public async Task ThrowsGivenNullQuantities()
{
var basketService = new BasketService(null, null);
await Assert.ThrowsAsync<ArgumentNullException>(async () =>
await basketService.SetQuantities(123, null));
}
}

View File

@@ -16,34 +16,19 @@ public class TransferBasket
private readonly string _nonexistentUserBasketBuyerId = "newuser@microsoft.com";
private readonly string _existentUserBasketBuyerId = "testuser@microsoft.com";
private readonly Mock<IRepository<Basket>> _mockBasketRepo = new();
[Fact]
public async Task ThrowsGivenNullAnonymousId()
{
var basketService = new BasketService(null, null);
await Assert.ThrowsAsync<ArgumentNullException>(async () => await basketService.TransferBasketAsync(null, "steve"));
}
[Fact]
public async Task ThrowsGivenNullUserId()
{
var basketService = new BasketService(null, null);
await Assert.ThrowsAsync<ArgumentNullException>(async () => await basketService.TransferBasketAsync("abcdefg", null));
}
private readonly Mock<IAppLogger<BasketService>> _mockLogger = new();
[Fact]
public async Task InvokesBasketRepositoryFirstOrDefaultAsyncOnceIfAnonymousBasketNotExists()
{
var anonymousBasket = null as Basket;
var userBasket = new Basket(_existentUserBasketBuyerId);
_mockBasketRepo.SetupSequence(x => x.GetBySpecAsync(It.IsAny<BasketWithItemsSpecification>(), default))
_mockBasketRepo.SetupSequence(x => x.FirstOrDefaultAsync(It.IsAny<BasketWithItemsSpecification>(), default))
.ReturnsAsync(anonymousBasket)
.ReturnsAsync(userBasket);
var basketService = new BasketService(_mockBasketRepo.Object, null);
var basketService = new BasketService(_mockBasketRepo.Object, _mockLogger.Object);
await basketService.TransferBasketAsync(_nonexistentAnonymousBasketBuyerId, _existentUserBasketBuyerId);
_mockBasketRepo.Verify(x => x.GetBySpecAsync(It.IsAny<BasketWithItemsSpecification>(), default), Times.Once);
_mockBasketRepo.Verify(x => x.FirstOrDefaultAsync(It.IsAny<BasketWithItemsSpecification>(), default), Times.Once);
}
[Fact]
@@ -55,10 +40,10 @@ public class TransferBasket
var userBasket = new Basket(_existentUserBasketBuyerId);
userBasket.AddItem(1, 10, 4);
userBasket.AddItem(2, 99, 3);
_mockBasketRepo.SetupSequence(x => x.GetBySpecAsync(It.IsAny<BasketWithItemsSpecification>(), default))
_mockBasketRepo.SetupSequence(x => x.FirstOrDefaultAsync(It.IsAny<BasketWithItemsSpecification>(), default))
.ReturnsAsync(anonymousBasket)
.ReturnsAsync(userBasket);
var basketService = new BasketService(_mockBasketRepo.Object, null);
var basketService = new BasketService(_mockBasketRepo.Object, _mockLogger.Object);
await basketService.TransferBasketAsync(_nonexistentAnonymousBasketBuyerId, _existentUserBasketBuyerId);
_mockBasketRepo.Verify(x => x.UpdateAsync(userBasket, default), Times.Once);
Assert.Equal(3, userBasket.Items.Count);
@@ -72,10 +57,10 @@ public class TransferBasket
{
var anonymousBasket = new Basket(_existentAnonymousBasketBuyerId);
var userBasket = new Basket(_existentUserBasketBuyerId);
_mockBasketRepo.SetupSequence(x => x.GetBySpecAsync(It.IsAny<BasketWithItemsSpecification>(), default))
_mockBasketRepo.SetupSequence(x => x.FirstOrDefaultAsync(It.IsAny<BasketWithItemsSpecification>(), default))
.ReturnsAsync(anonymousBasket)
.ReturnsAsync(userBasket);
var basketService = new BasketService(_mockBasketRepo.Object, null);
var basketService = new BasketService(_mockBasketRepo.Object, _mockLogger.Object);
await basketService.TransferBasketAsync(_nonexistentAnonymousBasketBuyerId, _existentUserBasketBuyerId);
_mockBasketRepo.Verify(x => x.UpdateAsync(userBasket, default), Times.Once);
_mockBasketRepo.Verify(x => x.DeleteAsync(anonymousBasket, default), Times.Once);
@@ -86,10 +71,10 @@ public class TransferBasket
{
var anonymousBasket = new Basket(_existentAnonymousBasketBuyerId);
var userBasket = null as Basket;
_mockBasketRepo.SetupSequence(x => x.GetBySpecAsync(It.IsAny<BasketWithItemsSpecification>(), default))
_mockBasketRepo.SetupSequence(x => x.FirstOrDefaultAsync(It.IsAny<BasketWithItemsSpecification>(), default))
.ReturnsAsync(anonymousBasket)
.ReturnsAsync(userBasket);
var basketService = new BasketService(_mockBasketRepo.Object, null);
var basketService = new BasketService(_mockBasketRepo.Object, _mockLogger.Object);
await basketService.TransferBasketAsync(_existentAnonymousBasketBuyerId, _nonexistentUserBasketBuyerId);
_mockBasketRepo.Verify(x => x.AddAsync(It.Is<Basket>(x => x.BuyerId == _nonexistentUserBasketBuyerId), default), Times.Once);
}

View File

@@ -1,6 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Xunit;
namespace Microsoft.eShopWeb.UnitTests.ApplicationCore.Specifications;
@@ -12,9 +10,7 @@ public class CatalogFilterPaginatedSpecification
{
var spec = new eShopWeb.ApplicationCore.Specifications.CatalogFilterPaginatedSpecification(0, 10, null, null);
var result = GetTestCollection()
.AsQueryable()
.Where(spec.WhereExpressions.FirstOrDefault().Filter);
var result = spec.Evaluate(GetTestCollection());
Assert.NotNull(result);
Assert.Equal(4, result.ToList().Count);
@@ -25,9 +21,7 @@ public class CatalogFilterPaginatedSpecification
{
var spec = new eShopWeb.ApplicationCore.Specifications.CatalogFilterPaginatedSpecification(0, 10, 1, 1);
var result = GetTestCollection()
.AsQueryable()
.Where(spec.WhereExpressions.FirstOrDefault().Filter);
var result = spec.Evaluate(GetTestCollection()).ToList();
Assert.NotNull(result);
Assert.Equal(2, result.ToList().Count);

View File

@@ -19,9 +19,7 @@ public class CatalogFilterSpecification
{
var spec = new eShopWeb.ApplicationCore.Specifications.CatalogFilterSpecification(brandId, typeId);
var result = GetTestItemCollection()
.AsQueryable()
.Where(spec.WhereExpressions.FirstOrDefault().Filter);
var result = spec.Evaluate(GetTestItemCollection()).ToList();
Assert.Equal(expectedCount, result.Count());
}

View File

@@ -14,9 +14,7 @@ public class CatalogItemsSpecification
var catalogItemIds = new int[] { 1 };
var spec = new eShopWeb.ApplicationCore.Specifications.CatalogItemsSpecification(catalogItemIds);
var result = GetTestCollection()
.AsQueryable()
.Where(spec.WhereExpressions.FirstOrDefault().Filter);
var result = spec.Evaluate(GetTestCollection()).ToList();
Assert.NotNull(result);
Assert.Single(result.ToList());
@@ -28,9 +26,7 @@ public class CatalogItemsSpecification
var catalogItemIds = new int[] { 1, 3 };
var spec = new eShopWeb.ApplicationCore.Specifications.CatalogItemsSpecification(catalogItemIds);
var result = GetTestCollection()
.AsQueryable()
.Where(spec.WhereExpressions.FirstOrDefault().Filter);
var result = spec.Evaluate(GetTestCollection()).ToList();
Assert.NotNull(result);
Assert.Equal(2, result.ToList().Count);

View File

@@ -15,14 +15,12 @@ public class CustomerOrdersWithItemsSpecification
{
var spec = new eShopWeb.ApplicationCore.Specifications.CustomerOrdersWithItemsSpecification(_buyerId);
var result = GetTestCollection()
.AsQueryable()
.FirstOrDefault(spec.WhereExpressions.FirstOrDefault().Filter);
var result = spec.Evaluate(GetTestCollection()).FirstOrDefault();
Assert.NotNull(result);
Assert.NotNull(result.OrderItems);
Assert.Equal(1, result.OrderItems.Count);
Assert.NotNull(result.OrderItems.FirstOrDefault().ItemOrdered);
Assert.NotNull(result.OrderItems.FirstOrDefault()?.ItemOrdered);
}
[Fact]
@@ -30,15 +28,12 @@ public class CustomerOrdersWithItemsSpecification
{
var spec = new eShopWeb.ApplicationCore.Specifications.CustomerOrdersWithItemsSpecification(_buyerId);
var result = GetTestCollection()
.AsQueryable()
.Where(spec.WhereExpressions.FirstOrDefault().Filter)
.ToList();
var result = spec.Evaluate(GetTestCollection()).ToList();
Assert.NotNull(result);
Assert.Equal(2, result.Count);
Assert.Equal(1, result[0].OrderItems.Count);
Assert.NotNull(result[0].OrderItems.FirstOrDefault().ItemOrdered);
Assert.NotNull(result[0].OrderItems.FirstOrDefault()?.ItemOrdered);
Assert.Equal(2, result[1].OrderItems.Count);
Assert.NotNull(result[1].OrderItems.ToList()[0].ItemOrdered);
Assert.NotNull(result[1].OrderItems.ToList()[1].ItemOrdered);

View File

@@ -22,7 +22,7 @@ public class GetOrderDetails
Order order = new Order("buyerId", address, new List<OrderItem> { item });
_mockOrderRepository = new Mock<IReadRepository<Order>>();
_mockOrderRepository.Setup(x => x.GetBySpecAsync(It.IsAny<OrderWithItemsByIdSpec>(), default))
_mockOrderRepository.Setup(x => x.FirstOrDefaultAsync(It.IsAny<OrderWithItemsByIdSpec>(), default))
.ReturnsAsync(order);
}