DainnProductEAV.PostgreSQL
9.0.0
dotnet add package DainnProductEAV.PostgreSQL --version 9.0.0
NuGet\Install-Package DainnProductEAV.PostgreSQL -Version 9.0.0
<PackageReference Include="DainnProductEAV.PostgreSQL" Version="9.0.0" />
<PackageVersion Include="DainnProductEAV.PostgreSQL" Version="9.0.0" />
<PackageReference Include="DainnProductEAV.PostgreSQL" />
paket add DainnProductEAV.PostgreSQL --version 9.0.0
#r "nuget: DainnProductEAV.PostgreSQL, 9.0.0"
#:package DainnProductEAV.PostgreSQL@9.0.0
#addin nuget:?package=DainnProductEAV.PostgreSQL&version=9.0.0
#tool nuget:?package=DainnProductEAV.PostgreSQL&version=9.0.0
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
- Installation
- Quick Start
- Configuration
- Database Providers
- Elasticsearch Integration
- Sample Data
- Usage Examples
- API Reference
- Extending the Library
- 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.SqlServerMicrosoft.EntityFrameworkCore.SqliteNpgsql.EntityFrameworkCore.PostgreSQLPomelo.EntityFrameworkCore.MySqlMicrosoft.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 productPOST /api/v1/catalog/products/reindex- Reindex all productsDELETE /api/v1/catalog/products/{id}/index- Remove product from indexGET /api/v1/catalog/products/elasticsearch/status- Check Elasticsearch status
Best Practices
- Initial Setup - Run a full reindex after initial data migration
- Bulk Operations - Use bulk indexing for multiple products instead of individual calls
- Error Handling - Elasticsearch operations gracefully handle failures - check return values
- Index Management - Create the index before indexing products (done automatically on first index)
- Store-Specific Indexing - Use store IDs for multi-store scenarios
- 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
- Use Attribute Codes Consistently - Define attribute codes once and reuse them
- Leverage Caching - Attribute metadata is cached for 30 minutes by default
- Use Store IDs for Multi-Language - Store ID 0 = global, >0 = store-specific
- Batch Attribute Updates - Use
SetAttributesAsync()instead of multipleSetAttributeAsync()calls - Create Attribute Sets for Product Types - Different product types may need different attributes
- Index Frequently Filtered Attributes - Mark attributes as
IsFilterablefor better query performance - Elasticsearch Indexing - Run full reindex after bulk data imports or schema changes
- Monitor Elasticsearch Health - Check Elasticsearch availability regularly, especially in production
- Use Bulk Operations - Prefer bulk indexing operations for better performance when indexing multiple products
License
MIT License - See LICENSE file for details.
| Product | Versions 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. |
-
net9.0
- DainnCommon (>= 9.0.0)
- Microsoft.EntityFrameworkCore (>= 9.0.0)
- Microsoft.EntityFrameworkCore.InMemory (>= 9.0.0)
- Microsoft.EntityFrameworkCore.Sqlite (>= 9.0.0)
- Microsoft.EntityFrameworkCore.SqlServer (>= 9.0.0)
- Microsoft.Extensions.Caching.Memory (>= 9.0.0)
- Microsoft.Extensions.Configuration.Binder (>= 9.0.0)
- Microsoft.Extensions.Configuration.Json (>= 9.0.0)
- NEST (>= 7.17.5)
- Npgsql.EntityFrameworkCore.PostgreSQL (>= 9.0.0)
- Pomelo.EntityFrameworkCore.MySql (>= 9.0.0)
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 |