WebServiceToolkit
A .NET library to simplify the development of ASP.NET Core web services.
WebServiceToolkit provides utilities for query binding, exception handling, dependency injection, and pagination support. It reduces boilerplate code and helps you build clean, maintainable ASP.NET Core web APIs.
Core Features
Query Binding
Automatic conversion of query parameters into strongly-typed classes with attribute-based configuration.
Exception Handling
Pre-built HTTP exception types with standardized response mapping to status codes.
Service Registration
Attribute-driven automatic dependency injection for cleaner service setup.
Pagination
Built-in models for paginated, sortable, and searchable collections.
Installation
Main Package (includes dependencies):
dotnet add package DevInstance.WebServiceToolkit
Or install individually:
dotnet add package DevInstance.WebServiceToolkit.Common
dotnet add package DevInstance.WebServiceToolkit.Database
Available Packages
| Package | Description |
|---|---|
DevInstance.WebServiceToolkit |
Main package with query binding, exception handling, and service registration |
DevInstance.WebServiceToolkit.Common |
Common models including ModelItem, ModelList, and query attributes |
DevInstance.WebServiceToolkit.Database |
Database query interfaces for pagination, search, and sorting |
License
WebServiceToolkit is released under the MIT License, allowing free usage, modification, and distribution.
Documentation
WebServiceToolkit Usage Guide
A comprehensive guide to using the WebServiceToolkit library for building ASP.NET Core web services with query binding, exception handling, and pagination support.
Table of Contents
- Installation & Setup
- Query Model Binding
- Service Registration
- Exception Handling
- Pagination Support
- Database Query Interfaces
- Complete Examples
Installation & Setup
Requirements
- .NET 10 SDK or later
Package Installation
Install the main package (includes all dependencies):
dotnet add package DevInstance.WebServiceToolkit
Or install packages individually:
dotnet add package DevInstance.WebServiceToolkit.Common
dotnet add package DevInstance.WebServiceToolkit.Database
Basic Setup
Configure services in Program.cs:
using DevInstance.WebServiceToolkit.Http.Query;
using DevInstance.WebServiceToolkit.Tools;
var builder = WebApplication.CreateBuilder(args);
// Add controllers with query model binding support
builder.Services.AddControllers()
.AddWebServiceToolkitQuery();
// Register services marked with [WebService] attribute
builder.Services.AddServerWebServices();
var app = builder.Build();
app.MapControllers();
app.Run();
Query Model Binding
Query model binding automatically converts URL query parameters into strongly-typed C# classes.
Defining a Query Model
Use the [QueryModel] attribute to mark a class for automatic binding:
using DevInstance.WebServiceToolkit.Common.Query;
[QueryModel]
public class ProductQuery
{
[DefaultValue(0)]
public int Page { get; set; }
[DefaultValue(20)]
public int PageSize { get; set; }
public string? Search { get; set; }
[QueryName("sort")]
public string? SortBy { get; set; }
public bool? IsActive { get; set; }
}
Query Attributes
| Attribute | Description |
|---|---|
[QueryModel] |
Marks a class for query model binding |
[QueryName("name")] |
Customizes the query parameter name |
[DefaultValue(value)] |
Sets a default value when parameter is not provided |
Using Query Models in Controllers
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet]
public async Task<ActionResult<ModelList<Product>>> GetProducts(ProductQuery query)
{
// query.Page, query.PageSize, query.Search are automatically bound
// from URL: /api/products?page=1&pageSize=10&search=laptop
var products = await _service.GetProductsAsync(query);
return Ok(products);
}
}
Array Parameters
Query models support array parameters using comma-separated values:
[QueryModel]
public class FilterQuery
{
public string[]? Categories { get; set; } // ?categories=electronics,books,toys
public int[]? Ids { get; set; } // ?ids=1,2,3,4,5
}
Service Registration
WebServiceToolkit provides attribute-based automatic service registration for dependency injection.
Marking Services
Use the [WebService] attribute to mark services for automatic registration:
using DevInstance.WebServiceToolkit.Tools;
[WebService]
public class ProductService : IProductService
{
private readonly IProductRepository _repository;
public ProductService(IProductRepository repository)
{
_repository = repository;
}
public async Task<ModelList<Product>> GetProductsAsync(ProductQuery query)
{
return await _repository.GetPagedAsync(query.Page, query.PageSize, query.Search);
}
}
Registration in Program.cs
// Automatically registers all classes marked with [WebService]
builder.Services.AddServerWebServices();
Services are registered with Scoped lifetime by default.
Exception Handling
WebServiceToolkit provides pre-built exception types that map to standard HTTP status codes.
Exception Types
| Exception | HTTP Status | Description |
|---|---|---|
BadRequestException |
400 | Invalid request data |
UnauthorizedException |
401 | Authentication required |
ForbiddenException |
403 | Access denied |
RecordNotFoundException |
404 | Resource not found |
RecordConflictException |
409 | Resource conflict (e.g., duplicate) |
Using Exceptions in Services
[WebService]
public class ProductService : IProductService
{
public async Task<Product> GetByIdAsync(string id)
{
var product = await _repository.FindAsync(id);
if (product == null)
{
throw new RecordNotFoundException($"Product with ID '{id}' not found");
}
return product;
}
public async Task<Product> CreateAsync(Product product)
{
if (string.IsNullOrWhiteSpace(product.Name))
{
throw new BadRequestException("Product name is required");
}
var existing = await _repository.FindByNameAsync(product.Name);
if (existing != null)
{
throw new RecordConflictException($"Product '{product.Name}' already exists");
}
return await _repository.CreateAsync(product);
}
}
Handling Exceptions in Controllers
Use HandleWebRequestAsync to automatically convert exceptions to appropriate HTTP responses:
using DevInstance.WebServiceToolkit.Http;
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _service;
public ProductsController(IProductService service)
{
_service = service;
}
[HttpGet("{id}")]
public Task<ActionResult<Product>> GetById(string id)
{
return this.HandleWebRequestAsync<Product>(async () =>
{
var product = await _service.GetByIdAsync(id);
return Ok(product);
});
}
[HttpPost]
public Task<ActionResult<Product>> Create([FromBody] Product product)
{
return this.HandleWebRequestAsync<Product>(async () =>
{
var created = await _service.CreateAsync(product);
return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
});
}
}
Pagination Support
WebServiceToolkit provides built-in models for paginated, sortable, and searchable collections.
ModelList
The ModelList<T> class wraps paginated results:
using DevInstance.WebServiceToolkit.Common;
public class ModelList<T>
{
public List<T> Items { get; set; }
public int TotalCount { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
}
Creating Paginated Responses
[WebService]
public class ProductService : IProductService
{
public async Task<ModelList<Product>> GetProductsAsync(ProductQuery query)
{
var totalCount = await _repository.CountAsync(query.Search);
var items = await _repository.GetPagedAsync(
query.Page,
query.PageSize,
query.Search,
query.SortBy
);
return new ModelList<Product>
{
Items = items,
TotalCount = totalCount,
Page = query.Page,
PageSize = query.PageSize
};
}
}
ModelItem Base Class
Use ModelItem as a base class for entities with an ID:
using DevInstance.WebServiceToolkit.Common;
public class Product : ModelItem
{
// Inherits: public string Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
}
Database Query Interfaces
The Database package provides fluent interfaces for building database queries.
IModelQuery<T, D>
Base interface for CRUD operations:
public interface IModelQuery<T, D> where T : ModelItem
{
Task<T?> FindAsync(string id);
Task<List<T>> GetAllAsync();
Task<T> CreateAsync(T item);
Task<T> UpdateAsync(T item);
Task DeleteAsync(string id);
}
IQPageable
Interface for pagination:
public interface IQPageable<T>
{
IQPageable<T> Skip(int count);
IQPageable<T> Take(int count);
Task<List<T>> ToListAsync();
Task<int> CountAsync();
}
IQSearchable<T, K>
Interface for search functionality:
public interface IQSearchable<T, K>
{
IQSearchable<T, K> Search(string term);
IQSearchable<T, K> Filter(Expression<Func<T, bool>> predicate);
}
IQSortable
Interface for sorting:
public interface IQSortable<T>
{
IQSortable<T> OrderBy<K>(Expression<Func<T, K>> keySelector);
IQSortable<T> OrderByDescending<K>(Expression<Func<T, K>> keySelector);
}
Combining Interfaces
public class ProductRepository : IProductRepository
{
private readonly DbContext _context;
public async Task<ModelList<Product>> GetPagedAsync(ProductQuery query)
{
var queryable = _context.Products.AsQueryable();
// Apply search
if (!string.IsNullOrEmpty(query.Search))
{
queryable = queryable.Where(p =>
p.Name.Contains(query.Search) ||
p.Description.Contains(query.Search));
}
// Apply sorting
queryable = query.SortBy switch
{
"name" => queryable.OrderBy(p => p.Name),
"price" => queryable.OrderBy(p => p.Price),
"price_desc" => queryable.OrderByDescending(p => p.Price),
_ => queryable.OrderBy(p => p.Name)
};
var totalCount = await queryable.CountAsync();
var items = await queryable
.Skip(query.Page * query.PageSize)
.Take(query.PageSize)
.ToListAsync();
return new ModelList<Product>
{
Items = items,
TotalCount = totalCount,
Page = query.Page,
PageSize = query.PageSize
};
}
}
Complete Examples
Full Controller Example
using DevInstance.WebServiceToolkit.Http;
using DevInstance.WebServiceToolkit.Common;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _service;
public ProductsController(IProductService service)
{
_service = service;
}
[HttpGet]
public Task<ActionResult<ModelList<Product>>> GetAll(ProductQuery query)
{
return this.HandleWebRequestAsync<ModelList<Product>>(async () =>
{
var products = await _service.GetProductsAsync(query);
return Ok(products);
});
}
[HttpGet("{id}")]
public Task<ActionResult<Product>> GetById(string id)
{
return this.HandleWebRequestAsync<Product>(async () =>
{
var product = await _service.GetByIdAsync(id);
return Ok(product);
});
}
[HttpPost]
public Task<ActionResult<Product>> Create([FromBody] Product product)
{
return this.HandleWebRequestAsync<Product>(async () =>
{
var created = await _service.CreateAsync(product);
return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
});
}
[HttpPut("{id}")]
public Task<ActionResult<Product>> Update(string id, [FromBody] Product product)
{
return this.HandleWebRequestAsync<Product>(async () =>
{
product.Id = id;
var updated = await _service.UpdateAsync(product);
return Ok(updated);
});
}
[HttpDelete("{id}")]
public Task<ActionResult> Delete(string id)
{
return this.HandleWebRequestAsync<object>(async () =>
{
await _service.DeleteAsync(id);
return NoContent();
});
}
}
Full Service Example
using DevInstance.WebServiceToolkit.Tools;
using DevInstance.WebServiceToolkit.Common;
using DevInstance.WebServiceToolkit.Http;
[WebService]
public class ProductService : IProductService
{
private readonly IProductRepository _repository;
private readonly ILogger<ProductService> _logger;
public ProductService(IProductRepository repository, ILogger<ProductService> logger)
{
_repository = repository;
_logger = logger;
}
public async Task<ModelList<Product>> GetProductsAsync(ProductQuery query)
{
_logger.LogInformation("Fetching products: Page={Page}, PageSize={PageSize}",
query.Page, query.PageSize);
return await _repository.GetPagedAsync(query);
}
public async Task<Product> GetByIdAsync(string id)
{
var product = await _repository.FindAsync(id);
if (product == null)
{
throw new RecordNotFoundException($"Product with ID '{id}' not found");
}
return product;
}
public async Task<Product> CreateAsync(Product product)
{
ValidateProduct(product);
var existing = await _repository.FindByNameAsync(product.Name);
if (existing != null)
{
throw new RecordConflictException($"Product '{product.Name}' already exists");
}
product.Id = Guid.NewGuid().ToString();
return await _repository.CreateAsync(product);
}
public async Task<Product> UpdateAsync(Product product)
{
ValidateProduct(product);
var existing = await _repository.FindAsync(product.Id);
if (existing == null)
{
throw new RecordNotFoundException($"Product with ID '{product.Id}' not found");
}
return await _repository.UpdateAsync(product);
}
public async Task DeleteAsync(string id)
{
var existing = await _repository.FindAsync(id);
if (existing == null)
{
throw new RecordNotFoundException($"Product with ID '{id}' not found");
}
await _repository.DeleteAsync(id);
}
private void ValidateProduct(Product product)
{
if (string.IsNullOrWhiteSpace(product.Name))
{
throw new BadRequestException("Product name is required");
}
if (product.Price < 0)
{
throw new BadRequestException("Product price cannot be negative");
}
}
}
API Reference
Common Package
| Type | Description |
|---|---|
ModelItem |
Base class with Id property |
ModelList<T> |
Paginated response wrapper |
QueryModelAttribute |
Marks classes for query binding |
QueryNameAttribute |
Customizes parameter names |
DefaultValueAttribute |
Sets default parameter values |
Main Package
| Type | Description |
|---|---|
QueryModelBinder |
Query string parsing engine |
BadRequestException |
HTTP 400 errors |
UnauthorizedException |
HTTP 401 errors |
ForbiddenException |
HTTP 403 errors |
RecordNotFoundException |
HTTP 404 errors |
RecordConflictException |
HTTP 409 errors |
ControllerUtils |
Exception handling extension methods |
WebServiceAttribute |
Marks services for DI registration |
Database Package
| Type | Description |
|---|---|
IModelQuery<T,D> |
Base CRUD operations interface |
IQPageable<T> |
Skip/Take pagination interface |
IQSearchable<T,K> |
Search functionality interface |
IQSortable<T> |
Column sorting interface |