DainnProductEAV.PostgreSQL 9.0.0

dotnet add package DainnProductEAV.PostgreSQL --version 9.0.0
                    
NuGet\Install-Package DainnProductEAV.PostgreSQL -Version 9.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DainnProductEAV.PostgreSQL" Version="9.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DainnProductEAV.PostgreSQL" Version="9.0.0" />
                    
Directory.Packages.props
<PackageReference Include="DainnProductEAV.PostgreSQL" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DainnProductEAV.PostgreSQL --version 9.0.0
                    
#r "nuget: DainnProductEAV.PostgreSQL, 9.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DainnProductEAV.PostgreSQL@9.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DainnProductEAV.PostgreSQL&version=9.0.0
                    
Install as a Cake Addin
#tool nuget:?package=DainnProductEAV.PostgreSQL&version=9.0.0
                    
Install as a Cake Tool

DainnProductEAV.PostgreSQL

A .NET 8 class library implementing the Entity-Attribute-Value (EAV) pattern for product catalog management, similar to Magento's architecture. This library provides a flexible, extensible product catalog system that allows adding custom attributes without modifying the database schema.

Features

  • ✅ EAV Pattern - Dynamic attributes stored in type-specific tables (varchar, int, decimal, text, datetime)
  • ✅ Multi-Store Support - Store-specific attribute values with global fallback
  • ✅ Multi-Provider Support - SQLite, SQL Server, PostgreSQL, MySQL, InMemory
  • ✅ Attribute Sets & Groups - Organize attributes like Magento
  • ✅ Category Management - Hierarchical categories with nested set model
  • ✅ Inventory Management - Stock tracking per product
  • ✅ Tier Pricing - Customer group and website-specific pricing
  • ✅ Media Gallery - Product images and videos
  • ✅ Product Relations - Configurable, grouped, related, upsell, crosssell products
  • ✅ Caching - Built-in memory caching for attribute metadata
  • ✅ Elasticsearch Integration - Full-text search and product indexing with Elasticsearch
  • ✅ Extensible - Easy to extend with custom repositories and services
  • ✅ Guid Primary Keys - All entities use Guid (UUID) for primary keys
  • ✅ PascalCase Tables - Consistent PascalCase naming convention for all database tables

Table of Contents

  1. Installation
  2. Quick Start
  3. Configuration
  4. Database Providers
  5. Elasticsearch Integration
  6. Sample Data
  7. Usage Examples
  8. API Reference
  9. Extending the Library
  10. Database Schema

Installation

1. Add Project Reference

If you have the source code:

dotnet add reference ../DainnProductEAV.PostgreSQL/DainnProductEAV.PostgreSQL.csproj

Or add as a NuGet package (when published):

dotnet add package DainnProductEAV.PostgreSQL

2. Required Dependencies

The library includes these EF Core providers:

  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Sqlite
  • Npgsql.EntityFrameworkCore.PostgreSQL
  • Pomelo.EntityFrameworkCore.MySql
  • Microsoft.EntityFrameworkCore.InMemory

For Elasticsearch integration (optional):

  • NEST (Elasticsearch .NET client)

Quick Start

Step 1: Add Configuration to appsettings.json

{
  "ProductCatalog": {
    "Provider": "sqlite",
    "ConnectionString": "Data Source=productcatalog.db",
    "AutoMigrate": true,
    "SeedDefaultAttributes": true
  }
}

Step 2: Register Services in Program.cs

using DainnProductEAV.PostgreSQL.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Add Product Catalog services
builder.Services.AddProductCatalog(builder.Configuration);

var app = builder.Build();

// Apply database migrations
app.Services.MigrateProductCatalogDatabase();

app.Run();

Step 3: Use the Services

using DainnProductEAV.PostgreSQL.Services;

public class ProductController : ControllerBase
{
    private readonly IProductService _productService;

    public ProductController(IProductService productService)
    {
        _productService = productService;
    }

    [HttpPost]
    public async Task<IActionResult> CreateProduct()
    {
        // Create a product
        var product = await _productService.CreateProductAsync("SKU-001", "simple", Guid.Parse("11111111-1111-1111-1111-111111111111"));

        // Set attributes
        await _productService.SetAttributeAsync(product.EntityId, "name", "My Product");
        await _productService.SetAttributeAsync(product.EntityId, "price", 29.99m);
        await _productService.SetAttributeAsync(product.EntityId, "status", 1);

        return Ok(product);
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetProduct(Guid id)
    {
        var product = await _productService.GetProductAsync(id);
        var name = await _productService.GetAttributeAsync<string>(id, "name");
        var price = await _productService.GetAttributeAsync<decimal>(id, "price");

        return Ok(new { product, name, price });
    }
}

Configuration

Configuration Options

Option Type Default Description
Provider string "sqlite" Database provider: sqlite, sqlserver, postgresql, mysql, inmemory
ConnectionString string Required Database connection string
AutoMigrate bool true Automatically create/migrate database on startup
SeedDefaultAttributes bool true Seed default product attributes

Example Configurations

SQLite
{
  "ProductCatalog": {
    "Provider": "sqlite",
    "ConnectionString": "Data Source=products.db"
  }
}
SQL Server
{
  "ProductCatalog": {
    "Provider": "sqlserver",
    "ConnectionString": "Server=localhost;Database=ProductCatalog;Trusted_Connection=true;TrustServerCertificate=true"
  }
}
PostgreSQL
{
  "ProductCatalog": {
    "Provider": "postgresql",
    "ConnectionString": "Host=localhost;Database=productcatalog;Username=postgres;Password=password"
  }
}
MySQL
{
  "ProductCatalog": {
    "Provider": "mysql",
    "ConnectionString": "Server=localhost;Database=productcatalog;User=root;Password=password"
  }
}
InMemory (for Testing)
{
  "ProductCatalog": {
    "Provider": "inmemory",
    "ConnectionString": "ProductCatalogTestDb"
  }
}

Elasticsearch Integration

The library includes full Elasticsearch integration for product indexing and search. Products are automatically indexed when created or updated, and removed from the index when deleted.

Features

  • ✅ Automatic Indexing - Products are automatically indexed on create/update
  • ✅ Bulk Operations - Efficient bulk indexing for multiple products
  • ✅ Full Reindexing - Reindex all products from the database
  • ✅ Custom Analyzers - Product-specific analyzers for better search
  • ✅ Dynamic Attributes - All EAV attributes are indexed in a flexible structure
  • ✅ Store Support - Store-specific indexing with fallback to global values

Configuration

Add Elasticsearch configuration to appsettings.json:

{
  "Elasticsearch": {
    "Enabled": true,
    "Url": "http://localhost:9200",
    "Username": null,
    "Password": null,
    "ProductIndexName": "products",
    "NumberOfShards": 1,
    "NumberOfReplicas": 0
  }
}
Configuration Options
Option Type Default Description
Enabled bool true Enable/disable Elasticsearch integration
Url string "http://localhost:9200" Elasticsearch server URL
Username string? null Username for authentication (optional)
Password string? null Password for authentication (optional)
ProductIndexName string "products" Name of the product index
NumberOfShards int 1 Number of shards for the index
NumberOfReplicas int 0 Number of replicas for the index

Index Document Structure

Products are indexed with the following structure:

{
  "id": "guid-string",
  "sku": "PRODUCT-SKU",
  "name": "Product Name",
  "description": "Product description",
  "typeId": "simple",
  "attributeSetId": "guid-string",
  "price": 29.99,
  "isInStock": true,
  "quantity": 100,
  "categoryIds": ["guid1", "guid2"],
  "createdAt": "2024-01-01T00:00:00Z",
  "updatedAt": "2024-01-01T00:00:00Z",
  "attributes": {
    "name": "Product Name",
    "price": 29.99,
    "status": 1,
    "color": "blue",
    // ... all EAV attributes
  }
}

Usage Examples

Automatic Indexing

Products are automatically indexed when created or updated:

public class ProductController : ControllerBase
{
    private readonly IProductService _productService;
    private readonly IProductIndexingService _indexingService;

    public ProductController(
        IProductService productService,
        IProductIndexingService indexingService)
    {
        _productService = productService;
        _indexingService = indexingService;
    }

    [HttpPost]
    public async Task<IActionResult> CreateProduct()
    {
        // Create product - automatically indexed
        var product = await _productService.CreateProductAsync("SKU-001", "simple", attributeSetId);
        
        // Set attributes - product will be reindexed automatically
        await _productService.SetAttributeAsync(product.EntityId, "name", "My Product");
        
        return Ok(product);
    }
}
Manual Indexing
public class IndexingExamples
{
    private readonly IProductIndexingService _indexingService;
    private readonly IProductService _productService;

    public IndexingExamples(
        IProductIndexingService indexingService,
        IProductService productService)
    {
        _indexingService = indexingService;
        _productService = productService;
    }

    // Index a single product
    public async Task IndexSingleProduct(Guid productId, int storeId = 0)
    {
        var product = await _productService.GetProductAsync(productId, storeId);
        if (product != null)
        {
            await _indexingService.IndexProductAsync(product, storeId);
        }
    }

    // Bulk index multiple products
    public async Task BulkIndexProducts(IEnumerable<ProductEntity> products, int storeId = 0)
    {
        var count = await _indexingService.IndexProductsAsync(products, storeId);
        Console.WriteLine($"Indexed {count} products");
    }

    // Reindex all products
    public async Task ReindexAll(int storeId = 0)
    {
        var count = await _indexingService.ReindexAllProductsAsync(storeId);
        Console.WriteLine($"Reindexed {count} products");
    }

    // Remove product from index
    public async Task RemoveFromIndex(Guid productId)
    {
        await _indexingService.RemoveProductAsync(productId);
    }
}
Elasticsearch Service Operations
public class ElasticsearchExamples
{
    private readonly IElasticsearchService _elasticsearchService;

    public ElasticsearchExamples(IElasticsearchService elasticsearchService)
    {
        _elasticsearchService = elasticsearchService;
    }

    // Check if Elasticsearch is available
    public async Task<bool> CheckAvailability()
    {
        return await _elasticsearchService.IsAvailableAsync();
    }

    // Create the product index
    public async Task CreateIndex()
    {
        var created = await _elasticsearchService.CreateProductIndexAsync();
        if (created)
        {
            Console.WriteLine("Index created successfully");
        }
    }

    // Check if index exists
    public async Task<bool> IndexExists()
    {
        return await _elasticsearchService.IndexExistsAsync();
    }

    // Delete the index (use with caution!)
    public async Task DeleteIndex()
    {
        await _elasticsearchService.DeleteProductIndexAsync();
    }

    // Access Elasticsearch client directly
    public async Task SearchProducts(string query)
    {
        var client = _elasticsearchService.Client;
        var response = await client.SearchAsync<ProductIndexDocument>(s => s
            .Query(q => q
                .MultiMatch(m => m
                    .Query(query)
                    .Fields(f => f
                        .Field(p => p.Name)
                        .Field(p => p.Description)
                        .Field(p => p.Sku)
                    )
                )
            )
        );

        if (response.IsValid)
        {
            var products = response.Documents;
            // Process search results
        }
    }
}

API Endpoints

When using the library with the API, the following endpoints are available:

  • POST /api/v1/catalog/products/{id}/index - Index a single product
  • POST /api/v1/catalog/products/reindex - Reindex all products
  • DELETE /api/v1/catalog/products/{id}/index - Remove product from index
  • GET /api/v1/catalog/products/elasticsearch/status - Check Elasticsearch status

Best Practices

  1. Initial Setup - Run a full reindex after initial data migration
  2. Bulk Operations - Use bulk indexing for multiple products instead of individual calls
  3. Error Handling - Elasticsearch operations gracefully handle failures - check return values
  4. Index Management - Create the index before indexing products (done automatically on first index)
  5. Store-Specific Indexing - Use store IDs for multi-store scenarios
  6. Monitoring - Check Elasticsearch status regularly to ensure availability

Disabling Elasticsearch

To disable Elasticsearch integration, set Enabled to false in configuration:

{
  "Elasticsearch": {
    "Enabled": false
  }
}

When disabled, all indexing operations will be no-ops and services will gracefully handle the absence of Elasticsearch.


Database Providers

Programmatic Configuration

You can also configure the provider programmatically:

// Using configuration
builder.Services.AddProductCatalog(builder.Configuration);

// Or with explicit options
builder.Services.AddProductCatalog(options =>
{
    options.Provider = "postgresql";
    options.ConnectionString = "Host=localhost;Database=products;...";
    options.AutoMigrate = true;
});

// Or with custom DbContext options
builder.Services.AddProductCatalog(dbOptions =>
{
    dbOptions.UseNpgsql("your-connection-string", npgsql =>
    {
        npgsql.EnableRetryOnFailure();
        npgsql.CommandTimeout(30);
    });
});

Sample Data

The library includes a sample data seeder for testing and demonstration purposes. It creates realistic sample products, categories, and attribute values.

Seeding Sample Data

using DainnProductEAV.PostgreSQL.Extensions;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddProductCatalog(builder.Configuration);

var app = builder.Build();

// Apply database migrations
app.Services.MigrateProductCatalogDatabase();

// Seed sample data (optional - for testing/demo)
app.Services.SeedSampleData();

app.Run();

Or use the async version:

await app.Services.SeedSampleDataAsync();

Sample Categories

The seeder creates a hierarchical category structure:

Electronics/
├── Computers & Laptops
├── Smartphones & Tablets
└── Audio & Headphones

Clothing/
├── Men's Clothing
└── Women's Clothing

Home & Garden/
├── Furniture
└── Home Decor

Sample Products

SKU Name Category Price
LAPTOP-001 ProBook Laptop 15.6" Computers & Laptops $1,299.99
PHONE-001 Galaxy Ultra Pro 5G Smartphones & Tablets $999.99
TABLET-001 iPad Pro 12.9" Smartphones & Tablets $1,099.00
AUDIO-001 NoiseCancel Pro Headphones Audio & Headphones $349.99
AUDIO-002 BoomBox Portable Speaker Audio & Headphones $199.99
SHIRT-M-001 Classic Oxford Button-Down Shirt Men's Clothing $79.99
PANTS-M-001 Slim Fit Chino Pants Men's Clothing $89.99
DRESS-W-001 Elegant Summer Maxi Dress Women's Clothing $129.99
BLOUSE-W-001 Silk Blend Blouse Women's Clothing $99.99
SOFA-001 Modern 3-Seater Sofa Furniture $899.99
TABLE-001 Solid Oak Dining Table Furniture $1,299.99
LAMP-001 Scandinavian Floor Lamp Home Decor $149.99
VASE-001 Ceramic Artisan Vase Home Decor $59.99
CUSHION-001 Velvet Throw Cushion Set Home Decor $49.99

Sample Product Data Includes

Each sample product comes with:

  • ✅ Core Attributes - name, description, short_description, url_key
  • ✅ Pricing - price, special_price (where applicable)
  • ✅ Status - Enabled and visible in catalog/search
  • ✅ Weight - Shipping weight
  • ✅ SEO - meta_title, meta_description
  • ✅ Inventory - Stock quantity and in-stock status
  • ✅ Category Assignment - Assigned to appropriate category

Note

The sample data seeder checks if products already exist before seeding. It will skip seeding if the database already contains products to avoid duplicates.


Usage Examples

Working with Products

public class ProductExamples
{
    private readonly IProductService _productService;

    public ProductExamples(IProductService productService)
    {
        _productService = productService;
    }

    // Create a product
    public async Task CreateProduct()
    {
        var product = await _productService.CreateProductAsync(
            sku: "SHIRT-001",
            typeId: "simple",      // simple, configurable, bundle, grouped
            attributeSetId: Guid.Parse("11111111-1111-1111-1111-111111111111")  // Default attribute set
        );

        // Set multiple attributes at once
        await _productService.SetAttributesAsync(product.EntityId, new Dictionary<string, object?>
        {
            ["name"] = "Blue Cotton Shirt",
            ["description"] = "A comfortable blue cotton shirt",
            ["price"] = 49.99m,
            ["status"] = 1,        // 1 = Enabled, 0 = Disabled
            ["visibility"] = 4,    // 4 = Catalog & Search
            ["weight"] = 0.5m,
            ["color"] = 1,         // Color option ID
            ["size"] = 2           // Size option ID
        });
    }

    // Get product with attributes
    public async Task<object> GetProductDetails(Guid productId, int storeId = 0)
    {
        var product = await _productService.GetProductAsync(productId, storeId);
        
        // Get individual attributes
        var name = await _productService.GetAttributeAsync<string>(productId, "name", storeId);
        var price = await _productService.GetAttributeAsync<decimal>(productId, "price", storeId);
        var description = await _productService.GetAttributeAsync<string>(productId, "description", storeId);

        return new { product, name, price, description };
    }

    // Update product
    public async Task UpdateProduct(Guid productId)
    {
        var product = await _productService.GetProductAsync(productId);
        product!.Sku = "SHIRT-001-UPDATED";
        await _productService.UpdateProductAsync(product);

        // Update attributes
        await _productService.SetAttributeAsync(productId, "price", 39.99m);
    }

    // Search products
    public async Task<IEnumerable<object>> SearchProducts(string query)
    {
        return await _productService.SearchProductsAsync(query, storeId: 0, page: 1, pageSize: 20);
    }

    // Delete product
    public async Task DeleteProduct(Guid productId)
    {
        await _productService.DeleteProductAsync(productId);
    }
}

Working with Categories

public class CategoryExamples
{
    private readonly ICategoryService _categoryService;
    private readonly IProductService _productService;

    public CategoryExamples(ICategoryService categoryService, IProductService productService)
    {
        _categoryService = categoryService;
        _productService = productService;
    }

    // Create category hierarchy
    public async Task CreateCategories()
    {
        // Create root category
        var clothing = await _categoryService.CreateCategoryAsync("Clothing", parentId: null);

        // Create child categories
        var shirts = await _categoryService.CreateCategoryAsync("Shirts", parentId: clothing.EntityId);
        var pants = await _categoryService.CreateCategoryAsync("Pants", parentId: clothing.EntityId);
        var tshirts = await _categoryService.CreateCategoryAsync("T-Shirts", parentId: shirts.EntityId);
    }

    // Note: All IDs (productId, categoryId, attributeId, etc.) are of type Guid

    // Get category tree
    public async Task<IEnumerable<object>> GetCategoryTree()
    {
        return await _categoryService.GetCategoryTreeAsync(rootId: null);
    }

    // Assign product to category
    public async Task AssignProductToCategory(Guid productId, Guid categoryId)
    {
        await _productService.AssignProductToCategoryAsync(productId, categoryId, position: 0);
    }

    // Get products in category
    public async Task<IEnumerable<object>> GetProductsInCategory(Guid categoryId)
    {
        return await _productService.GetProductsByCategoryAsync(categoryId, storeId: 0, page: 1, pageSize: 20);
    }

    // Move category
    public async Task MoveCategory(Guid categoryId, Guid? newParentId)
    {
        await _categoryService.MoveCategoryAsync(categoryId, newParentId, position: 0);
    }
}

Working with Attributes

public class AttributeExamples
{
    private readonly IAttributeService _attributeService;

    public AttributeExamples(IAttributeService attributeService)
    {
        _attributeService = attributeService;
    }

    // Create custom attribute
    public async Task CreateCustomAttribute()
    {
        var attribute = await _attributeService.CreateAttributeAsync(
            attributeCode: "brand",
            backendType: "varchar",      // varchar, int, decimal, text, datetime
            frontendInput: "select",     // text, textarea, select, multiselect, boolean, date, price
            frontendLabel: "Brand",
            isRequired: false,
            isSearchable: true,
            isFilterable: true,
            attributeGroupId: Guid.Parse("11111111-1111-1111-1111-111111111111")  // General group
        );
    }

    // Create attribute set (e.g., for Electronics)
    public async Task CreateAttributeSet()
    {
        // Create the set
        var attributeSet = await _attributeService.CreateAttributeSetAsync("Electronics");

        // Create groups within the set
        var specsGroup = await _attributeService.CreateAttributeGroupAsync(
            name: "Specifications",
            attributeSetId: attributeSet.AttributeSetId,
            sortOrder: 1
        );

        // Create attributes for this group
        await _attributeService.CreateAttributeAsync(
            attributeCode: "screen_size",
            backendType: "decimal",
            frontendInput: "text",
            frontendLabel: "Screen Size (inches)",
            attributeGroupId: specsGroup.AttributeGroupId
        );
    }

    // Get all attributes
    public async Task<IEnumerable<object>> GetAllAttributes()
    {
        return await _attributeService.GetAllAttributesAsync();
    }
}

Working with Inventory

public class InventoryExamples
{
    private readonly IProductService _productService;

    public InventoryExamples(IProductService productService)
    {
        _productService = productService;
    }

    // Update inventory
    public async Task UpdateInventory(Guid productId, int qty, bool isInStock)
    {
        await _productService.UpdateInventoryAsync(productId, qty, isInStock);
    }

    // Get inventory
    public async Task<object?> GetInventory(Guid productId)
    {
        return await _productService.GetInventoryAsync(productId);
    }
}

Multi-Store Support

public class MultiStoreExamples
{
    private readonly IProductService _productService;

    public MultiStoreExamples(IProductService productService)
    {
        _productService = productService;
    }

    // Set store-specific attribute value
    public async Task SetStoreSpecificValue(Guid productId)
    {
        // Set global value (storeId = 0)
        await _productService.SetAttributeAsync(productId, "name", "Blue Shirt", storeId: 0);

        // Set French store value (storeId = 1)
        await _productService.SetAttributeAsync(productId, "name", "Chemise Bleue", storeId: 1);

        // Set German store value (storeId = 2)
        await _productService.SetAttributeAsync(productId, "name", "Blaues Hemd", storeId: 2);
    }

    // Get store-specific value (with global fallback)
    public async Task<string?> GetStoreSpecificValue(Guid productId, int storeId)
    {
        // If store-specific value exists, returns it
        // Otherwise falls back to global value (storeId = 0)
        return await _productService.GetAttributeAsync<string>(productId, "name", storeId);
    }
}

API Reference

Note: All ID parameters (productId, categoryId, attributeId, attributeSetId, attributeGroupId, etc.) are of type Guid (UUID), not int.

IProductService

Method Description
GetProductAsync(id, storeId) Get product by ID (id: Guid)
GetProductBySkuAsync(sku, storeId) Get product by SKU
GetProductsAsync(storeId, page, pageSize) Get paginated products
CreateProductAsync(sku, typeId, attributeSetId) Create new product (attributeSetId: Guid)
UpdateProductAsync(product) Update product
DeleteProductAsync(id) Delete product (id: Guid)
GetAttributeAsync<T>(productId, attributeCode, storeId) Get attribute value (productId: Guid)
SetAttributeAsync(productId, attributeCode, value, storeId) Set attribute value (productId: Guid)
SetAttributesAsync(productId, attributes, storeId) Set multiple attributes (productId: Guid)
GetProductsByCategoryAsync(categoryId, storeId, page, pageSize) Get products by category (categoryId: Guid)
AssignProductToCategoryAsync(productId, categoryId, position) Assign to category (both IDs: Guid)
RemoveProductFromCategoryAsync(productId, categoryId) Remove from category (both IDs: Guid)
UpdateInventoryAsync(productId, qty, isInStock) Update inventory (productId: Guid)
GetInventoryAsync(productId) Get inventory (productId: Guid)
SearchProductsAsync(query, storeId, page, pageSize) Search products

ICategoryService

Method Description
GetCategoryAsync(id) Get category by ID (id: Guid)
GetAllCategoriesAsync() Get all categories
GetRootCategoriesAsync() Get root categories
GetCategoryTreeAsync(rootId) Get category tree (rootId: Guid?)
CreateCategoryAsync(name, parentId) Create category (parentId: Guid?)
UpdateCategoryAsync(category) Update category
DeleteCategoryAsync(id) Delete category (id: Guid)
MoveCategoryAsync(categoryId, newParentId, position) Move category (both IDs: Guid)

IAttributeService

Method Description
GetAttributeAsync(id) Get attribute by ID (id: Guid)
GetAttributeByCodeAsync(code) Get attribute by code
GetAllAttributesAsync() Get all attributes
CreateAttributeAsync(...) Create attribute (attributeGroupId: Guid?)
UpdateAttributeAsync(attribute) Update attribute
DeleteAttributeAsync(id) Delete attribute (id: Guid)
GetAttributeSetAsync(id) Get attribute set (id: Guid)
GetAllAttributeSetsAsync() Get all attribute sets
CreateAttributeSetAsync(name) Create attribute set
GetAttributeGroupAsync(id) Get attribute group (id: Guid)
GetAttributeGroupsBySetAsync(setId) Get groups by set (setId: Guid)
CreateAttributeGroupAsync(name, setId, sortOrder) Create attribute group (setId: Guid)

IProductIndexingService

Method Description
IndexProductAsync(product, storeId) Index a single product
IndexProductsAsync(products, storeId) Bulk index multiple products
RemoveProductAsync(productId) Remove product from index (productId: Guid)
RemoveProductsAsync(productIds) Bulk remove products from index
ReindexAllProductsAsync(storeId) Reindex all products from database
ConvertToIndexDocumentAsync(product, storeId) Convert ProductEntity to ProductIndexDocument

IElasticsearchService

Method Description
Client Get the Elasticsearch client (IElasticClient)
IsAvailableAsync() Check if Elasticsearch is available
CreateProductIndexAsync() Create the product index
DeleteProductIndexAsync() Delete the product index
IndexExistsAsync() Check if the product index exists

Extending the Library

Custom Product Repository

public class CustomProductRepository : ProductRepository
{
    public CustomProductRepository(ProductCatalogDbContext context, IMemoryCache cache)
        : base(context, cache)
    {
    }

    // Add custom methods
    public async Task<IEnumerable<ProductEntity>> GetFeaturedProductsAsync()
    {
        // Custom implementation
    }

    // Override existing methods
    public override async Task<ProductEntity> CreateAsync(ProductEntity product, CancellationToken ct = default)
    {
        // Add custom logic before creation
        product.CreatedAt = DateTime.UtcNow;
        
        return await base.CreateAsync(product, ct);
    }
}

// Register in Program.cs
builder.Services.AddProductCatalog<CustomProductRepository>(builder.Configuration);

Custom Product Service

public class CustomProductService : ProductService
{
    private readonly ILogger<CustomProductService> _logger;

    public CustomProductService(
        IProductRepository repository,
        ProductCatalogDbContext context,
        ILogger<CustomProductService> logger)
        : base(repository, context)
    {
        _logger = logger;
    }

    public override async Task<ProductEntity> CreateProductAsync(
        string sku, string typeId = "simple", Guid attributeSetId = default, CancellationToken ct = default)
    {
        _logger.LogInformation("Creating product with SKU: {Sku}", sku);
        
        var product = await base.CreateProductAsync(sku, typeId, attributeSetId, ct);
        
        // Add custom logic after creation
        _logger.LogInformation("Product created with ID: {Id}", product.EntityId);
        
        return product;
    }
}

// Register in Program.cs
builder.Services.AddProductCatalogWithCustomServices<
    CustomProductService,
    CategoryService,
    AttributeService>(builder.Configuration);

Database Schema

Core Tables

Table Description
ProductEntity Main product table (SKU, type, attribute set)
EavAttribute Attribute definitions
EavAttributeSet Attribute sets (e.g., Default, Electronics)
EavAttributeGroup Groups within sets (e.g., General, Prices)

EAV Value Tables

Table Data Type Example Attributes
ProductEntityVarchar String (max 255) name, url_key, manufacturer
ProductEntityInt Integer status, visibility, color_id
ProductEntityDecimal Decimal price, weight, special_price
ProductEntityText Long text description, short_description
ProductEntityDatetime DateTime special_from_date, news_from_date

Other Tables

Table Description
CategoryEntity Product categories
ProductCategory Product-category relations
ProductInventory Stock information
ProductPrice Tier pricing
ProductMediaGallery Images and videos
ProductRelation Product relationships
ProductWebsite Multi-website assignments

Note: All primary keys use Guid (UUID) type. Table names use PascalCase convention.


Default Seed Attributes

The library seeds these default attributes:

Code Type Label
name varchar Product Name
description text Description
short_description text Short Description
price decimal Price
special_price decimal Special Price
status int Status
visibility int Visibility
weight decimal Weight
url_key varchar URL Key
meta_title varchar Meta Title
meta_description text Meta Description
meta_keywords text Meta Keywords
image varchar Base Image
small_image varchar Small Image
thumbnail varchar Thumbnail
color int Color
size int Size
manufacturer varchar Manufacturer
special_from_date datetime Special Price From
special_to_date datetime Special Price To

Best Practices

  1. Use Attribute Codes Consistently - Define attribute codes once and reuse them
  2. Leverage Caching - Attribute metadata is cached for 30 minutes by default
  3. Use Store IDs for Multi-Language - Store ID 0 = global, >0 = store-specific
  4. Batch Attribute Updates - Use SetAttributesAsync() instead of multiple SetAttributeAsync() calls
  5. Create Attribute Sets for Product Types - Different product types may need different attributes
  6. Index Frequently Filtered Attributes - Mark attributes as IsFilterable for better query performance
  7. Elasticsearch Indexing - Run full reindex after bulk data imports or schema changes
  8. Monitor Elasticsearch Health - Check Elasticsearch availability regularly, especially in production
  9. Use Bulk Operations - Prefer bulk indexing operations for better performance when indexing multiple products

License

MIT License - See LICENSE file for details.

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
9.0.0 247 12/25/2025