Get Single Example
Overview
This document demonstrates the complete Get Single flow using the GetShippingSingle feature from the EBusiness application. The flow starts with a GET request to the controller and uses Query projects instead of Handlers in the DomainHandler section for data retrieval. This returns a single entity based on specific criteria, similar to Get By ID but with more flexible filtering.
Complete Flow Architecture
GET Request β Controller β Service β Query Handler β RESTClient β External System
β
Response β Controller β Service β Query Handler β RESTClient β External System
Detailed Flow Breakdown
1. GET Request
β
2. Controller (API Entry Point)
β
3. Service Layer (Business Orchestration)
β
4. Query Handler (RESTClient Integration)
βββ Internal DTO β Request DTO Mapping
βββ RESTClient Call to External System
βββ Response DTO β Internal DTO Mapping
β
5. Response (Single Entity)
Step-by-Step Implementation
1. API Controller - The Entry Point
File: ShippingController_GetShippingSingle.cs
[HttpGet()]
[Route("GetShippingSingle")]
[ProducesResponseType(typeof(GetShippingSingleDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetShippingSingle([FromQuery]GetShippingSingleParams parameters)
{
return RunQuerySingleService<GetShippingSingleParams, GetShippingSingleDto>(
parameters, _processShippingService.GetShippingSingle);
}
What Happens:
HTTP Method:
GET /api/Shipping/GetShippingSingle
Input:
GetShippingSingleParams
from query parametersAction: Calls the service layer to process the query
Response: HTTP 200 OK with
GetShippingSingleDto
or 404 Not Found
2. Service Layer - Business Orchestration
File: ProcessShippingService_GetShippingSingle.cs
public GetShippingSingleDto GetShippingSingle(GetShippingSingleParams @params)
{
return _flexHost.GetFlexiQuery<GetShippingSingle>().AssignParameters(@params).Fetch();
}
What Happens:
Query Resolution: Gets the FlexiQuery instance for GetShippingSingle
Parameter Assignment: Assigns query parameters to the query handler
Query Execution: Calls the Fetch() method to execute the query
Result: Returns single shipping or null if not found
3. Query Handler - RESTClient Integration
File: GetShippingSingle.cs
public class GetShippingSingle : FlexiQueryBridge<Shipping, GetShippingSingleDto>
{
protected readonly ILogger<GetShippingSingle> _logger;
protected GetShippingSingleParams _params;
protected readonly ShippingRESTClient _restClient;
public GetShippingSingle(ILogger<GetShippingSingle> logger, ShippingRESTClient restClient)
{
_logger = logger;
_restClient = restClient;
}
public virtual GetShippingSingle AssignParameters(GetShippingSingleParams @params)
{
_params = @params;
return this;
}
public override GetShippingSingleDto Fetch()
{
try
{
// Convert internal parameters to Request DTO
GetShippingSingleRequestDto requestParams = new GetShippingSingleRequestDto
{
Id = _params.Id,
OrderId = _params.OrderId,
TrackingNumber = _params.TrackingNumber,
Status = _params.Status?.ToString(),
CreatedDate = _params.CreatedDate,
CreatedBy = _params.CreatedBy
};
// Call external REST service
var response = _restClient.GetShippingSingle(requestParams).Result;
if (response.IsSuccessStatusCode)
{
var responseContent = response.Content.ReadAsStringAsync().Result;
var responseDto = JsonConvert.DeserializeObject<GetShippingSingleResponseDto>(responseContent);
// Convert Response DTO to Internal DTO
var result = new GetShippingSingleDto
{
Id = responseDto.Id,
OrderId = responseDto.OrderId,
ShippingAddress = responseDto.ShippingAddress,
TrackingNumber = responseDto.TrackingNumber,
Status = ParseShippingStatus(responseDto.Status),
StatusDescription = responseDto.StatusDescription,
TotalWeight = responseDto.TotalWeight,
TotalVolume = responseDto.TotalVolume,
CreatedDate = responseDto.CreatedDate,
LastModifiedDate = responseDto.LastModifiedDate,
CreatedBy = responseDto.CreatedBy,
LastModifiedBy = responseDto.LastModifiedBy,
ShippingItems = responseDto.ShippingItems?.Select(x => new ShippingItemDto
{
ProductId = x.ProductId,
Quantity = x.Quantity,
Weight = x.Weight,
Volume = x.Volume
}).ToList()
};
_logger.LogDebug("Shipping single retrieved successfully for criteria: {Criteria}",
_params.Id ?? _params.OrderId ?? _params.TrackingNumber);
return result;
}
else
{
_logger.LogWarning("Shipping not found for criteria. Status: {StatusCode}", response.StatusCode);
return null;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving shipping single for criteria: {Criteria}",
_params.Id ?? _params.OrderId ?? _params.TrackingNumber);
return null;
}
}
private ShippingStatus ParseShippingStatus(string status)
{
return status?.ToLower() switch
{
"created" => ShippingStatus.Created,
"in_transit" => ShippingStatus.InTransit,
"delivered" => ShippingStatus.Delivered,
"cancelled" => ShippingStatus.Cancelled,
_ => ShippingStatus.Unknown
};
}
}
What Happens:
Parameter Assignment: Stores query parameters for use in RESTClient calls
Request Mapping: Converts internal parameters to Request DTO
RESTClient Call: Calls external shipping service via RESTClient
Response Mapping: Converts Response DTO to Internal DTO
Error Handling: Handles success/failure responses from external system
Data Transformation: Applies business logic during mapping
4. Query Parameters - Input DTO
File: GetShippingSingleParams.cs
public class GetShippingSingleParams : DtoBridge
{
// Primary identifier (optional - can use other criteria)
public string Id { get; set; }
// Alternative filtering criteria
public string OrderId { get; set; }
public string TrackingNumber { get; set; }
public ShippingStatus? Status { get; set; }
// Additional criteria
public DateTime? CreatedDate { get; set; }
public string CreatedBy { get; set; }
}
What Happens:
Flexible Parameters: Multiple ways to identify the entity
Primary ID: Can use ID for direct lookup
Alternative Criteria: Can use OrderId, TrackingNumber, etc.
Query Parameters: Automatically bound from URL query string
5. Output DTO - Data Transfer Object
File: GetShippingSingleDto.cs
public partial class GetShippingSingleDto : DtoBridge
{
public string Id { get; set; }
public string OrderId { get; set; }
public string ShippingAddress { get; set; }
public string TrackingNumber { get; set; }
public ShippingStatus Status { get; set; }
public string StatusDescription { get; set; }
public decimal TotalWeight { get; set; }
public decimal TotalVolume { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime? LastModifiedDate { get; set; }
public string CreatedBy { get; set; }
public string LastModifiedBy { get; set; }
public ICollection<ShippingItemDto> ShippingItems { get; set; }
}
What Happens:
Complete Data: Contains all relevant fields for the entity
Detailed Structure: Includes all properties for full entity display
Related Data: May include related entity information (ShippingItems)
Audit Fields: Includes creation and modification tracking
6. AutoMapper Configuration - Data Transformation
File: GetShippingSingleMapperConfiguration.cs
public partial class GetShippingSingleMapperConfiguration : FlexMapperProfile
{
public GetShippingSingleMapperConfiguration() : base()
{
CreateMap<Shipping, GetShippingSingleDto>()
.ForMember(d => d.StatusDescription, opt => opt.MapFrom(s => s.Status.ToString()))
.ForMember(d => d.ShippingItems, opt => opt.MapFrom(s => s.ShippingItems));
}
}
What Happens:
Entity to DTO Mapping: Maps domain entities to DTOs
Custom Mappings: Handles complex property transformations
Related Data: Maps related entity properties
Computed Fields: Handles calculated properties
Key Differences from Get By ID
Get Single vs Get By ID Characteristics
Input
Query parameters
URL parameter (ID)
Filtering
Multiple criteria
Single ID only
Flexibility
High (multiple ways to find)
Low (ID only)
Use Case
Complex lookups
Direct ID lookup
Performance
Depends on criteria
Very fast (indexed)
Controller Method
RunQuerySingleService
RunQuerySingleService
Query Base
FlexiQueryBridge
FlexiQueryBridge
Get Single-Specific Features
Flexible Filtering: Multiple ways to identify the entity
Complex Criteria: Can combine multiple filter conditions
Business Logic: Can implement complex lookup rules
Alternative Keys: Can use non-primary key fields
Conditional Logic: Can apply different logic based on parameters
Common Use Cases
Lookup by Alternative Key: Find by tracking number, order ID, etc.
Conditional Lookups: Different logic based on user role or context
Business Rules: Complex business logic for entity identification
API Integration: Flexible lookup for external systems
Search Scenarios: Find entity based on multiple criteria
Flow Summary
Synchronous Flow (Data Retrieval)
GET Request β Controller receives request with query parameters
Service Processing β Business orchestration and query resolution
Query Handler β Database query building and execution
AutoMapper β Entity-to-DTO transformation
Response β HTTP 200 OK with entity or 404 Not Found
No Asynchronous Flow
No Events: Query operations don't publish events
No Subscribers: No side effects or event processing
Immediate Response: Data is returned immediately
Query Building Patterns
Basic Query Building
protected override IQueryable<T> Build<T>()
{
_repoFactory.Init(_params);
IQueryable<T> query = _repoFactory.GetRepo().FindAll<T>();
// Primary ID lookup
if (!string.IsNullOrEmpty(_params.Id))
{
query = query.Where(s => s.Id == _params.Id);
}
// Alternative criteria
else if (!string.IsNullOrEmpty(_params.OrderId))
{
query = query.Where(s => s.OrderId == _params.OrderId);
}
else if (!string.IsNullOrEmpty(_params.TrackingNumber))
{
query = query.Where(s => s.TrackingNumber == _params.TrackingNumber);
}
return query;
}
Advanced Query Building with Multiple Criteria
protected override IQueryable<T> Build<T>()
{
_repoFactory.Init(_params);
IQueryable<T> query = _repoFactory.GetRepo().FindAll<T>();
// Exclude soft deleted items
query = query.Where(s => !s.IsSoftDeleted);
// Primary ID lookup (highest priority)
if (!string.IsNullOrEmpty(_params.Id))
{
query = query.Where(s => s.Id == _params.Id);
}
else
{
// Alternative lookup criteria
if (!string.IsNullOrEmpty(_params.OrderId))
{
query = query.Where(s => s.OrderId == _params.OrderId);
}
if (!string.IsNullOrEmpty(_params.TrackingNumber))
{
query = query.Where(s => s.TrackingNumber == _params.TrackingNumber);
}
if (_params.Status.HasValue)
{
query = query.Where(s => s.Status == _params.Status.Value);
}
if (_params.CreatedDate.HasValue)
{
query = query.Where(s => s.CreatedDate.Date == _params.CreatedDate.Value.Date);
}
if (!string.IsNullOrEmpty(_params.CreatedBy))
{
query = query.Where(s => s.CreatedBy == _params.CreatedBy);
}
}
// Order by creation date for consistent results
query = query.OrderByDescending(s => s.CreatedDate);
return query;
}
Business Logic Query Building
protected override IQueryable<T> Build<T>()
{
_repoFactory.Init(_params);
IQueryable<T> query = _repoFactory.GetRepo().FindAll<T>();
// Exclude soft deleted items
query = query.Where(s => !s.IsSoftDeleted);
// Business logic: Find active shipping for order
if (!string.IsNullOrEmpty(_params.OrderId))
{
query = query.Where(s => s.OrderId == _params.OrderId &&
s.Status != ShippingStatus.Cancelled &&
s.Status != ShippingStatus.Delivered);
}
// Business logic: Find shipping by tracking number (any status)
if (!string.IsNullOrEmpty(_params.TrackingNumber))
{
query = query.Where(s => s.TrackingNumber == _params.TrackingNumber);
}
// Business logic: Find latest shipping for user
if (!string.IsNullOrEmpty(_params.CreatedBy))
{
query = query.Where(s => s.CreatedBy == _params.CreatedBy)
.OrderByDescending(s => s.CreatedDate);
}
return query;
}
Error Handling
HTTP Status Codes
200 OK: Entity found and returned successfully
404 Not Found: Entity with specified criteria does not exist
400 Bad Request: Invalid parameters or missing required criteria
500 Internal Server Error: Database or server error
Controller Error Handling
[HttpGet()]
[Route("GetShippingSingle")]
[ProducesResponseType(typeof(GetShippingSingleDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> GetShippingSingle([FromQuery]GetShippingSingleParams parameters)
{
// Validate that at least one criteria is provided
if (string.IsNullOrEmpty(parameters.Id) &&
string.IsNullOrEmpty(parameters.OrderId) &&
string.IsNullOrEmpty(parameters.TrackingNumber))
{
return BadRequest("At least one search criteria must be provided");
}
return RunQuerySingleService<GetShippingSingleParams, GetShippingSingleDto>(
parameters, _processShippingService.GetShippingSingle);
}
Performance Considerations
Optimization Strategies
Database Indexing: Ensure proper indexes on filter fields
Query Optimization: Use efficient WHERE clauses
Parameter Validation: Validate parameters early
Caching: Consider caching for frequently accessed data
Query Planning: Plan queries based on most common use cases
When to Use Get Single vs Get By ID
Alternative Keys
β Yes
β No
Complex Lookups
β Yes
β No
Business Rules
β Yes
β No
Direct ID Lookup
β No
β Yes
Simple Lookups
β No
β Yes
Performance Critical
β No
β Yes
Key Benefits
Flexibility: Multiple ways to find entities
Business Logic: Can implement complex lookup rules
Alternative Keys: Support for non-primary key lookups
Type Safety: Strongly typed DTOs and parameters
AutoMapper: Automatic entity-to-DTO mapping
Error Handling: Built-in 404 handling for missing entities
No Side Effects: Read-only operations
Testable: Each component can be tested independently
Maintainable: Clear separation of concerns
This GetShippingSingle example demonstrates how FlexBase enables clean, maintainable, and scalable single entity retrieval operations with flexible filtering and business logic support! π
Last updated