Initial Project Structure:

  • Projects:
    • Shary.API
    • Shary.Core
    • Shary.Repository
    • Shary.Service

Core (Entities):

  • Create BaseEntity, ProductCategory, ProductBrand, Product
  • Product properties: Name, Description, Price(decimal), PictureUrl
  • Navigation: Category (with CategoryId), Brand (with BrandId)

Repository (Data Layer):

  • StoreContext : DbContext in Data folder
    • Constructor takes DbContextOptions and passes to base (DI)
    • Add DbSet for Products, ProductBrands, ProductCategories
    • OnModelCreating uses modelBuilder.ApplyConfigurationsFromAssembly(...)
  • Config Folder in Data:
    • ProductConfigurations : IEntityTypeConfiguration<Product>
    • In Configure:
      • builder.Property(p => p.Name).IsRequired().HasMaxLength(100)
      • builder.HasOne(p => p.Brand).WithMany().HasForeignKey(p => p.BrandId)
      • Configure all other properties and relationships similarly

Dependency Injection (in Shary.API Program):

builder.Services.AddDbContext<StoreContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

Migrations:

  • Run: add-migration "ProductModule" -O Data/Migrations
  • Update DB: update-database

Apply Migrations at runtime:

IServiceScope scope = app.Services.CreateScope();
StoreContext _dbContext = scope.ServiceProvider.GetRequiredService<StoreContext>();
ILoggerFactory loggerFactory = scope.ServiceProvider.GetRequiredService<ILoggerFactory>();
 
try
{
    await _dbContext.Database.MigrateAsync();
}
catch (Exception ex)
{
    ILogger<Program> logger = loggerFactory.CreateLogger<Program>();
    logger.LogError(ex, "Error during migration");
}

Data Seeding:

  • Add DataSeeding folder
  • Add StoreContextSeed class for seeding data

Generic Repository:

  • In Core Add Repositories.Contract:
    • Add IGenericRepository<T> where T : BaseEntity
      • Methods: GetAsync, GetAllAsync
  • In Repository:
    • Add GenericRepository<T> implementing IGenericRepository<T>
      • Inject StoreContext in the constructor

Controllers Setup:

  • In Shary.API under Controllers folder:
    • Create BaseApiController : ControllerBase with [Route("api/[controller]")] and [ApiController].
    • Create ProductsController : BaseApiController.
      • Inject IGenericRepository<Product> via constructor.
      • Add endpoint GetProducts returning Task<ActionResult<IEnumerable<Product>>>.
      • Use Include (through specifications) to include related entities.

Register Services:

builder.Services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>));

Specification Pattern:

  • In Shary.Core:
    • Create Specifications folder.
    • ISpecification<T> interface:
      • Expression<Func<T, bool>> Criteria { get; }
      • List<Expression<Func<T, object>>> Includes { get; }
    • BaseSpecifications<T> class implementing ISpecification<T>:
      • Handle Criteria and Includes lists.
      • Provide methods to add criteria and includes.

In Repository Layer:

  • Create SpecificationEvaluator class to apply Criteria and Includes to queries.
  • In GenericRepository add:
    • Task<IEnumerable<T>> GetAllWithSpecAsync(ISpecification<T> spec);
    • Task<T?> GetWithSpecAsync(ISpecification<T> spec);
    • Implement these methods using SpecificationEvaluator.

Example Specification:

  • ProductWithBrandAndCategorySpecification : BaseSpecifications<Product>
    • Add includes for Brand and Category.

Update Endpoints:

  • In ProductsController, use specification-based methods for GetProducts() to include Brand and Category.