Skip to content

CRUD Operations

GrydCrud provides five core operations that can be used independently or combined.

Operation Types

OperationInterfaceDescription
CreateICreateOperation<TEntity, TCreateDto, TResultDto>Create new entities
Read (Query)IQueryOperation<TEntity, TResultDto, TQueryParams>List with pagination
Read (GetById)IGetByIdOperation<TEntity, TResultDto>Get single entity
UpdateIUpdateOperation<TEntity, TUpdateDto, TResultDto>Update existing entity
DeleteIDeleteOperation<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

  1. Validation - Validates DTO using registered validator
  2. Mapping - Maps CreateDto to entity using IEntityMapper
  3. Save - Adds entity to repository and calls SaveChangesAsync
  4. Result - Maps entity to ResultDto and 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

  1. Find Entity - Loads entity by ID from repository
  2. Validation - Validates DTO using registered validator
  3. Mapping - Applies DTO changes to entity
  4. Save - Updates entity and calls SaveChangesAsync
  5. 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 = true and DeletedAt = 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=desc

For 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

  1. Keep Operations Focused - Each operation should do one thing well
  2. Use Hooks for Side Effects - Don't mix concerns in the main operation
  3. Return Early on Failure - Use Result pattern for clean error handling
  4. Include Only What's Needed - Don't over-fetch related entities
  5. Validate at the Right Level - Use FluentValidation for DTO validation, hooks for business rules

Released under the MIT License.