using APP.Domain;
using APP.Models;
using CORE.APP.Models;
using CORE.APP.Services;
using Microsoft.EntityFrameworkCore;
namespace APP.Services
{
// Inherit from the generic entity service class therefore DbContext injected constructor can be automatically created
// and entity CRUD (create, read, update, delete) methods in the base class can be invoked.
public class ProductService : Service<Product>, IService<ProductRequest, ProductResponse>
{
public ProductService(DbContext db) : base(db)
{
// if the culture of the application is needed to be changed for this service, below assignment can be made:
//CultureInfo = new CultureInfo("tr-TR"); default culture is defined as "en-US" in the base service class
}
// base virtual Query method is overriden therefore the overriden query can be used in all other methods
protected override IQueryable<Product> Query(bool isNoTracking = true)
{
// p: Product entity delegate, ps: ProductStore entity delegate
return base.Query(isNoTracking) // will return Products DbSet
.Include(p => p.Category) // will include the relational Category data
.Include(p => p.ProductStores).ThenInclude(ps => ps.Store) // will first include the relational ProductStores then Store data
.OrderByDescending(p => p.ExpirationDate) // query will be ordered descending by ExpirationDate values
.ThenByDescending(p => p.StockAmount) // after ExpirationDate ordering, query will be ordered descending by StockAmount values
.ThenBy(p => p.Name); // after StockAmount ordering, query will be ordered ascending by Name values
// Include, ThenInclude, OrderBy, OrderByDescending, ThenBy and ThenByDescending methods can also be used with DbSets.
/*
Relational entity data (Store, ProductStores) can be included to the query by using the Include method (Entity Framework Core Eager Loading).
If the included relational entity data (ProductStores) has a relation with other entity data (Store), ThenInclude method is used.
If you want to automatically include all relational data without using Include / ThenInclude methods (Entity Framework Core Lazy Loading),
you need to make the necessary configuration in the class inheriting from DbContext class (Db) to enable Lazy Loading (not recommended).
*/
}
public List<ProductResponse> List()
{
// get the query of all Product entities then project each entity to ProductResponse object and return the list of ProductResponse objects
return Query().Select(p => new ProductResponse // () after the class name may not be used
{
// assigning entity properties to the response
Id = p.Id,
Guid = p.Guid,
Name = p.Name,
UnitPrice = p.UnitPrice,
StockAmount = p.StockAmount,
ExpirationDate = p.ExpirationDate,
CategoryId = p.CategoryId,
StoreIds = p.StoreIds,
// assigning custom or formatted properties to the response
UnitPriceF = p.UnitPrice.ToString("C2"), // C: currency format, N: number format, 2: 2 decimal places
// If Product entity's ExpirationDate value is not null, convert and assign the value with month/day/year format, otherwise assign "".
// No need to give the second CultureInfo parameter (e.g. new CultureInfo("tr-TR")) to the ToString method since
// CultureInfo property was assigned in the constructor of the base or this class.
// Instead of ToString method, ToShortDateString (e.g. 08/30/2025) or ToLongDateString (e.g. Saturday, August 30, 2025) methods can be used.
// For time ToShortTimeString (13:21) or ToLongTimeString (13:21:57) can be used.
// Again CultureInfo parameter is not needed for these methods.
ExpirationDateF = p.ExpirationDate.HasValue ? p.ExpirationDate.Value.ToString("MM/dd/yyyy") : string.Empty,
// Way 1: Ternary Operator
//StockAmountF = (p.StockAmount.HasValue ? p.StockAmount.Value : 0).ToString(),
// Way 2:
StockAmountF = (p.StockAmount ?? 0).ToString(), // If p.StockAmount value is null use 0 otherwise use p.StockAmount value.
Category = p.Category.Title, // Assign the relational Category's Title value, if Category was optional meaning a product may not have a category,
// p.Category != null ? p.Category.Title : string.Empty must be written
Stores = p.ProductStores.Select(ps => ps.Store.Name).ToList() // Get store name values from the relational Store of each ProductStore (ps)
// and convert them to a list of string.
}).ToList();
}
// get a single Product entity by Id then project the entity to ProductResponse object and return the ProductResponse object
public ProductResponse Item(int id)
{
var entity = Query().SingleOrDefault(p => p.Id == id);
if (entity is null)
return null;
return new ProductResponse
{
// assigning entity properties to the response
Id = entity.Id,
Guid = entity.Guid,
Name = entity.Name,
UnitPrice = entity.UnitPrice,
StockAmount = entity.StockAmount,
ExpirationDate = entity.ExpirationDate,
CategoryId = entity.CategoryId,
StoreIds = entity.StoreIds,
// assigning custom or formatted properties to the response
UnitPriceF = entity.UnitPrice.ToString("C2"),
ExpirationDateF = entity.ExpirationDate.HasValue ? entity.ExpirationDate.Value.ToShortDateString() : string.Empty,
StockAmountF = (entity.StockAmount ?? 0).ToString(),
Category = entity.Category.Title,
Stores = entity.ProductStores.Select(ps => ps.Store.Name).ToList()
};
}
// get a single Product entity by Id then project the entity to ProductRequest object and return the ProductRequest object
public ProductRequest Edit(int id)
{
var entity = Query().SingleOrDefault(p => p.Id == id);
if (entity is null)
return null;
return new ProductRequest
{
// assigning entity properties to the request
Id = entity.Id,
Name = entity.Name,
UnitPrice = entity.UnitPrice,
StockAmount = entity.StockAmount,
ExpirationDate = entity.ExpirationDate,
CategoryId = entity.CategoryId,
StoreIds = entity.StoreIds
};
}
// create a new Product entity from the ProductRequest object and save it to the database if a product with the same name does not exist
public CommandResponse Create(ProductRequest request)
{
// p: Product entity delegate. Check if a product with the same name exists
// (case-sensitive, request.Name without white space characters in the beginning and at the end).
if (Query().Any(p => p.Name == request.Name.Trim()))
return Error("Product with the same name exists!");
var entity = new Product
{
Name = request.Name.Trim(), // remove white space characters in the beginning and at the end
UnitPrice = request.UnitPrice,
StockAmount = request.StockAmount,
ExpirationDate = request.ExpirationDate,
CategoryId = request.CategoryId ?? 0,
StoreIds = request.StoreIds
};
Create(entity); // will add the entity to the Products DbSet and since save default parameter's value is true, will save changes to the database
return Success("Product created successfully.", entity.Id);
}
// get the Product entity by Id then update the entity from the ProductRequest object and save changes to the database
// if a product other than the current updated product with the same name does not exist
public CommandResponse Update(ProductRequest request)
{
// p: Product entity delegate. Check if a product excluding the current updated product with the same name exists
// (case-sensitive, request.Name without white space characters in the beginning and at the end).
if (Query().Any(p => p.Id != request.Id && p.Name == request.Name.Trim()))
return Error("Product with the same name exists!");
// get the Product entity by ID from the Products table
var entity = Query(false).SingleOrDefault(p => p.Id == request.Id); // isNoTracking is false for being tracked by EF Core to update the entity
if (entity is null)
return Error("Product not found!");
// delete the relational ProductStore entities data since the stores of the product is updated from request.StoreIds below
Delete(entity.ProductStores);
// update retrieved Product entity's properties with request properties
entity.Name = request.Name.Trim();
entity.UnitPrice = request.UnitPrice;
entity.StockAmount = request.StockAmount;
entity.ExpirationDate = request.ExpirationDate;
entity.CategoryId = request.CategoryId ?? 0;
entity.StoreIds = request.StoreIds;
Update(entity); // will update the entity in the Products DbSet and since save default parameter's value is true, will save changes to the database
return Success("Product updated successfully.", entity.Id);
}
// get the Product entity by Id then delete the entity from the database
public CommandResponse Delete(int id)
{
// get the Product entity by ID from the Products table
var entity = Query(false).SingleOrDefault(p => p.Id == id); // isNoTracking is false for being tracked by EF Core to delete the entity
if (entity is null)
return Error("Product not found!");
// delete the relational ProductStore entities
Delete(entity.ProductStores);
// delete the Product entity
Delete(entity); // will delete the entity from the Products DbSet and since save default parameter's value is true, will save changes to the database
return Success("Product deleted successfully.", entity.Id);
}
// get a filtered list of Product response items filtered by the Product query request properties
public List<ProductResponse> List(ProductQueryRequest request)
{
// get the query of all Product entities
var query = Query();
// apply filtering according to the request properties if they have values, p: Product entity delegate
// if Name != null and Name.Trim() != ""
if (!string.IsNullOrWhiteSpace(request.Name))
// apply name filtering to the query for exact match
// Way 1:
//query = query.Where(u => p.Name.Equals(request.Name));
// Way 2:
//query = query.Where(p => p.Name == request.Name);
// Way 3: apply name filtering to the query for partial match
// (case-sensitive, without white space characters in the beginning and at the end)
query = query.Where(p => p.Name.Contains(request.Name.Trim()));
// if UnitPriceStart has a value
if (request.UnitPriceStart.HasValue)
// apply unit price start filtering to the query for greater than or equal match
query = query.Where(p => p.UnitPrice >= request.UnitPriceStart.Value);
// if UnitPriceEnd has a value
if (request.UnitPriceEnd.HasValue)
// apply unit price end filtering to the query for less than or equal match
query = query.Where(p => p.UnitPrice <= request.UnitPriceEnd.Value);
// if StockAmountStart has a value
if (request.StockAmountStart.HasValue)
// apply stock amount start filtering to the query for greater than or equal match
query = query.Where(p => (p.StockAmount ?? 0) >= request.StockAmountStart.Value);
// if p.StockAmount is null use 0 otherwise use p.StockAmount value
// if StockAmountEnd has a value
if (request.StockAmountEnd.HasValue)
// apply stock amount end filtering to the query for less than or equal match
query = query.Where(p => (p.StockAmount ?? 0) <= request.StockAmountEnd.Value);
// if ExpirationDateStart has a value
if (request.ExpirationDateStart.HasValue)
// apply expiration date start filtering to the query for greater than or equal match
// Way 1: filtering with date and time value (e.g. 08/22/1990 13:45:57)
//query = query.Where(u => u.ExpirationDateStart.HasValue && u.ExpirationDateStart.Value >= request.ExpirationDateStart.Value);
// Way 2: filtering with date value only (e.g. 08/22/1990)
query = query.Where(p => p.ExpirationDate.HasValue && p.ExpirationDate.Value.Date >= request.ExpirationDateStart.Value.Date);
// p.ExpirationDate.HasValue is checked because null values cannot be compared
// if ExpirationDateEnd has a value
if (request.ExpirationDateEnd.HasValue)
// apply expiration date end filtering to the query for less than or equal match
query = query.Where(p => p.ExpirationDate.HasValue && p.ExpirationDate.Value.Date <= request.ExpirationDateEnd.Value.Date);
// if CategoryId has a value
if (request.CategoryId.HasValue)
// apply category ID filtering to the query for exact match
query = query.Where(p => p.CategoryId == request.CategoryId.Value);
// if StoreIds has a list with at least one element
if (request.StoreIds.Count > 0) // Any() method can also be used instead of Count > 0
// apply store IDs filtering to the query for any match
query = query.Where(p => p.ProductStores.Any(ps => request.StoreIds.Contains(ps.StoreId)));
// check if any of the ProductStores store ID exist in the request's StoreIds list, ps: ProductStore entity delegate
// project each entity to ProductResponse object and return the list of ProductResponse objects
return query.Select(p => new ProductResponse
{
// assigning entity properties to the response
Id = p.Id,
Guid = p.Guid,
Name = p.Name,
UnitPrice = p.UnitPrice,
StockAmount = p.StockAmount,
ExpirationDate = p.ExpirationDate,
CategoryId = p.CategoryId,
StoreIds = p.StoreIds,
// assigning custom or formatted properties to the response
UnitPriceF = p.UnitPrice.ToString("C2"),
StockAmountF = (p.StockAmount ?? 0).ToString(),
ExpirationDateF = p.ExpirationDate.HasValue ? p.ExpirationDate.Value.ToShortDateString() : string.Empty,
Category = p.Category.Title,
Stores = p.ProductStores.OrderBy(ps => ps.Store.Name).Select(ps => ps.Store.Name).ToList() // ps: ProductStore entity delegate
}).ToList();
}
}
}