CRUD Operations
GrydCrud provides five core operations that can be used independently or combined.
Operation Types
| Operation | Interface | Description |
|---|---|---|
| Create | ICreateOperation<TEntity, TCreateDto, TResultDto> | Create new entities |
| Read (Query) | IQueryOperation<TEntity, TResultDto, TQueryParams> | List with pagination |
| Read (GetById) | IGetByIdOperation<TEntity, TResultDto> | Get single entity |
| Update | IUpdateOperation<TEntity, TUpdateDto, TResultDto> | Update existing entity |
| Delete | IDeleteOperation<TEntity> | Delete entity (soft/hard) |
Create Operation
Basic Usage
csharp
public class ProductService
{
private readonly ICreateOperation<Product, CreateProductDto, ProductDto> _createOperation;
public async Task<Result<ProductDto>> CreateAsync(CreateProductDto dto)
{
return await _createOperation.ExecuteAsync(dto);
}
}Default Behavior
- Validation - Validates DTO using registered validator
- Mapping - Maps
CreateDtoto entity usingIEntityMapper - Save - Adds entity to repository and calls
SaveChangesAsync - Result - Maps entity to
ResultDtoand returns success
Customizing with Hooks
csharp
public class CustomCreateOperation : DefaultCreateOperation<Product, CreateProductDto, ProductDto>
{
private readonly ICategoryRepository _categoryRepository;
public CustomCreateOperation(
IRepository<Product> repository,
IEntityMapper mapper,
IUnitOfWork unitOfWork,
CrudOptions options,
ICategoryRepository categoryRepository,
IValidator<CreateProductDto>? validator = null)
: base(repository, mapper, unitOfWork, options, validator)
{
_categoryRepository = categoryRepository;
}
// Called before validation
protected override async Task<Result> BeforeValidationAsync(
CreateProductDto dto,
CancellationToken cancellationToken)
{
// Verify category exists
var categoryExists = await _categoryRepository.ExistsAsync(dto.CategoryId, cancellationToken);
if (!categoryExists)
{
return Result.Failure("CATEGORY_NOT_FOUND", "Category does not exist");
}
return Result.Success();
}
// Called before saving
protected override async Task<Result> BeforeSaveAsync(
Product entity,
CreateProductDto dto,
CancellationToken cancellationToken)
{
// Set defaults or compute values
entity.Sku = await GenerateSkuAsync(entity.CategoryId, cancellationToken);
return Result.Success();
}
// Called after saving
protected override async Task AfterSaveAsync(
Product entity,
CreateProductDto dto,
CancellationToken cancellationToken)
{
// Send notifications, update caches, etc.
await _eventBus.PublishAsync(new ProductCreatedEvent(entity.Id));
}
}Registration
csharp
// Use custom operation
builder.Services.AddCrudFor<Product, ProductDto, CreateProductDto, UpdateProductDto>()
.UseCreateOperation<CustomCreateOperation>();Update Operation
Basic Usage
csharp
public class ProductService
{
private readonly IUpdateOperation<Product, UpdateProductDto, ProductDto> _updateOperation;
public async Task<Result<ProductDto>> UpdateAsync(Guid id, UpdateProductDto dto)
{
return await _updateOperation.ExecuteAsync(id, dto);
}
}Default Behavior
- Find Entity - Loads entity by ID from repository
- Validation - Validates DTO using registered validator
- Mapping - Applies DTO changes to entity
- Save - Updates entity and calls
SaveChangesAsync - Result - Returns updated entity as DTO
Customizing with Hooks
csharp
public class CustomUpdateOperation : DefaultUpdateOperation<Product, UpdateProductDto, ProductDto>
{
private readonly ICurrentUserService _currentUser;
public CustomUpdateOperation(
IRepository<Product> repository,
IEntityMapper mapper,
IUnitOfWork unitOfWork,
CrudOptions options,
ICurrentUserService currentUser,
IValidator<UpdateProductDto>? validator = null)
: base(repository, mapper, unitOfWork, options, validator)
{
_currentUser = currentUser;
}
// FindEntityAsync returns TEntity? (nullable), not Result<TEntity>
protected override async Task<Product?> FindEntityAsync(
Guid id,
CancellationToken cancellationToken)
{
// Custom loading - just return the entity or null
// The base operation handles the not found case
return await Repository.GetByIdAsync(id, cancellationToken);
}
protected override async Task<Result> BeforeUpdateAsync(
Product entity,
UpdateProductDto dto,
CancellationToken cancellationToken)
{
// Authorization check
if (entity.CreatedBy != _currentUser.UserId && !_currentUser.IsInRole("Admin"))
{
return Result.Failure("FORBIDDEN", "You can only edit your own products");
}
return Result.Success();
}
protected override async Task AfterUpdateAsync(
Product entity,
UpdateProductDto dto,
CancellationToken cancellationToken)
{
// Invalidate cache, send notifications, etc.
await _eventBus.PublishAsync(new ProductUpdatedEvent(entity.Id));
}
}Delete Operation
Basic Usage
csharp
public class ProductService
{
private readonly IDeleteOperation<Product> _deleteOperation;
public async Task<Result> DeleteAsync(Guid id)
{
return await _deleteOperation.ExecuteAsync(id);
}
}Soft Delete vs Hard Delete
GrydCrud supports both soft delete and hard delete:
csharp
// Configuration
builder.Services.AddGrydCrud(options =>
{
options.UseSoftDelete = true; // Default for entities implementing ISoftDeletable
});Soft Delete (ISoftDeletable entities):
- Sets
IsDeleted = trueandDeletedAt = DateTime.UtcNow - Entity remains in database
- Automatically filtered from queries
Hard Delete (non-ISoftDeletable or when forced):
- Permanently removes entity from database
Force Hard Delete
csharp
public class CustomDeleteOperation : DefaultDeleteOperation<Product>
{
protected override async Task<Result> DeleteEntityAsync(
Product entity,
CancellationToken cancellationToken)
{
// Force hard delete regardless of ISoftDeletable
await _repository.DeleteAsync(entity, cancellationToken);
return Result.Success();
}
}Customizing with Hooks
csharp
public class CustomDeleteOperation : DefaultDeleteOperation<Product>
{
private readonly IOrderRepository _orderRepository;
protected override async Task<Result> BeforeDeleteAsync(
Product entity,
CancellationToken cancellationToken)
{
// Check for dependencies
var hasOrders = await _orderRepository.ExistsWithProductAsync(entity.Id, cancellationToken);
if (hasOrders)
{
return Result.Failure("HAS_ORDERS", "Cannot delete product with existing orders");
}
return Result.Success();
}
protected override async Task AfterDeleteAsync(
Product entity,
CancellationToken cancellationToken)
{
// Clean up related data
await _imageStorage.DeleteProductImagesAsync(entity.Id);
// Notify
await _eventBus.PublishAsync(new ProductDeletedEvent(entity.Id));
}
}GetById Operation
Basic Usage
csharp
public class ProductService
{
private readonly IGetByIdOperation<Product, ProductDto> _getByIdOperation;
public async Task<Result<ProductDto>> GetByIdAsync(Guid id)
{
return await _getByIdOperation.ExecuteAsync(id);
}
}Customizing Entity Loading
csharp
public class CustomGetByIdOperation : DefaultGetByIdOperation<Product, ProductDto>
{
protected override async Task<Result<Product>> FindEntityAsync(
Guid id,
CancellationToken cancellationToken)
{
// Include related data
var product = await _repository
.AsQueryable()
.Include(p => p.Category)
.Include(p => p.Images)
.Include(p => p.Reviews)
.FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
if (product == null)
return Result<Product>.Failure("NOT_FOUND", "Product not found");
return Result<Product>.Success(product);
}
}Query Operation
Basic Usage
csharp
public class ProductService
{
private readonly IQueryOperation<Product, ProductDto, ProductQueryParameters> _queryOperation;
public async Task<PagedResult<ProductDto>> GetPagedAsync(ProductQueryParameters query)
{
return await _queryOperation.ExecuteAsync(query);
}
}Query Parameters
csharp
// Built-in QueryParameters
public class QueryParameters
{
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20;
public string? SortBy { get; set; }
public SortDirection SortDirection { get; set; } = SortDirection.Ascending;
public string? Search { get; set; }
public bool IncludeDeleted { get; set; } = false;
}
// Custom query parameters
public class ProductQueryParameters : QueryParameters
{
public Guid? CategoryId { get; set; }
public decimal? MinPrice { get; set; }
public decimal? MaxPrice { get; set; }
public bool? IsActive { get; set; }
public string[]? Tags { get; set; }
}Customizing Filtering
csharp
public class CustomQueryOperation : DefaultQueryOperation<Product, ProductDto, ProductQueryParameters>
{
protected override Expression<Func<Product, bool>> BuildFilterExpression(
ProductQueryParameters parameters)
{
return product =>
// Base soft delete filter
(!product.IsDeleted || parameters.IncludeDeleted) &&
// Custom filters
(!parameters.CategoryId.HasValue || product.CategoryId == parameters.CategoryId) &&
(!parameters.MinPrice.HasValue || product.Price >= parameters.MinPrice) &&
(!parameters.MaxPrice.HasValue || product.Price <= parameters.MaxPrice) &&
(!parameters.IsActive.HasValue || product.IsActive == parameters.IsActive) &&
(parameters.Tags == null || parameters.Tags.Any(t => product.Tags.Contains(t))) &&
// Search
(string.IsNullOrEmpty(parameters.Search) ||
product.Name.Contains(parameters.Search) ||
product.Description.Contains(parameters.Search));
}
protected override IQueryable<Product> ApplyIncludes(IQueryable<Product> query)
{
return query
.Include(p => p.Category)
.Include(p => p.Images.Take(1)); // Only first image for list
}
}Sorting
GrydCrud supports dynamic sorting:
http
GET /api/products?sortBy=price&sortDirection=desc
GET /api/products?sortBy=name&sortDirection=asc
GET /api/products?sortBy=createdAt&sortDirection=descFor custom sort mappings:
csharp
public class CustomQueryOperation : DefaultQueryOperation<Product, ProductDto, QueryParameters>
{
protected override IQueryable<Product> ApplySorting(
IQueryable<Product> query,
QueryParameters parameters)
{
return parameters.SortBy?.ToLower() switch
{
"price" => parameters.SortDirection == SortDirection.Ascending
? query.OrderBy(p => p.Price)
: query.OrderByDescending(p => p.Price),
"name" => parameters.SortDirection == SortDirection.Ascending
? query.OrderBy(p => p.Name)
: query.OrderByDescending(p => p.Name),
"popularity" => query.OrderByDescending(p => p.ViewCount), // Custom sort
_ => query.OrderByDescending(p => p.CreatedAt) // Default
};
}
}Combining Operations
Using CrudOperations Aggregator
csharp
public class ProductService
{
private readonly ICrudOperations<Product, ProductDto, CreateProductDto, UpdateProductDto, QueryParameters> _crud;
public async Task<PagedResult<ProductDto>> GetAllAsync(QueryParameters query)
=> await _crud.QueryAsync(query);
public async Task<Result<ProductDto>> GetByIdAsync(Guid id)
=> await _crud.GetByIdAsync(id);
public async Task<Result<ProductDto>> CreateAsync(CreateProductDto dto)
=> await _crud.CreateAsync(dto);
public async Task<Result<ProductDto>> UpdateAsync(Guid id, UpdateProductDto dto)
=> await _crud.UpdateAsync(id, dto);
public async Task<Result> DeleteAsync(Guid id)
=> await _crud.DeleteAsync(id);
}Using CrudService
csharp
// Registration
builder.Services.AddCrudFor<Product, ProductDto, CreateProductDto, UpdateProductDto>();
// Usage
public class ProductController : ControllerBase
{
private readonly ICrudService<Product, ProductDto, CreateProductDto, UpdateProductDto, QueryParameters> _service;
[HttpGet]
public async Task<IActionResult> GetAll([FromQuery] QueryParameters query)
{
var result = await _service.GetPagedAsync(query);
return Ok(result);
}
}Operation Lifecycle
┌──────────────────────────────────────────────────────────────┐
│ CREATE OPERATION │
├──────────────────────────────────────────────────────────────┤
│ BeforeValidationAsync(dto) │
│ ↓ │
│ Validate(dto) - FluentValidation │
│ ↓ │
│ Map(dto → entity) │
│ ↓ │
│ BeforeSaveAsync(entity, dto) │
│ ↓ │
│ Repository.AddAsync(entity) │
│ ↓ │
│ UnitOfWork.SaveChangesAsync() │
│ ↓ │
│ AfterSaveAsync(entity, dto) │
│ ↓ │
│ Map(entity → resultDto) │
│ ↓ │
│ Return Result<ResultDto> │
└──────────────────────────────────────────────────────────────┘Best Practices
- Keep Operations Focused - Each operation should do one thing well
- Use Hooks for Side Effects - Don't mix concerns in the main operation
- Return Early on Failure - Use Result pattern for clean error handling
- Include Only What's Needed - Don't over-fetch related entities
- Validate at the Right Level - Use FluentValidation for DTO validation, hooks for business rules