Shipment Service¶
The ShipmentAppService is the core service of the Hub module, managing all shipment-related operations including creation, updates, queries, and logistics tracking.
Overview¶
Location: modules/hub/src/Hub.Application/Services/ShipmentAppService.cs
Interface: IShipmentAppService
Base Class: HubEntityBaseService<Shipment, ShipmentDto>
Permissions: HubPermissions.Shipment.*
Features¶
- CRUD Operations: Complete create, read, update, delete functionality
- Advanced Filtering: Organization-based and attribute-based filtering
- Pagination: Efficient handling of large datasets
- Faceted Search: Dynamic facets for filtering
- Document Integration: Linked document management
- Multi-Organization: Tenant and organization isolation
Service Methods¶
Get Upcoming Shipments¶
Retrieves shipments that are departing within a specified date range.
Task<PagedResultDtoWithFilters<ShipmentDto>> GetUpcomingShipments(
PagedAndSortedResultRequestWithFacetOptionsDto input,
DateTime? startDate = null,
DateTime? endDate = null
)
Parameters:
- input: Pagination, sorting, and filtering options
- startDate: Optional start date (defaults to current UTC date)
- endDate: Optional end date (defaults to 14 days from now)
Returns: Paginated list of upcoming shipments with filters
Authorization: Requires HubPermissions.Shipment.View
Filters Applied: - Estimated departure within date range - Has estimated arrival - Has departure and arrival ports - Has transport mode (Air or Sea with FCL/LCL) - Has container mode
Example:
var result = await _shipmentAppService.GetUpcomingShipments(
new PagedAndSortedResultRequestWithFacetOptionsDto
{
SkipCount = 0,
MaxResultCount = 20,
Sorting = "EstimatedDeparture DESC",
IncludeFacets = true
},
startDate: DateTime.UtcNow,
endDate: DateTime.UtcNow.AddDays(30)
);
Console.WriteLine($"Found {result.TotalCount} upcoming shipments");
foreach (var shipment in result.Items)
{
Console.WriteLine($"Shipment {shipment.Id}: {shipment.FirstConsolLegDeparturePort} → {shipment.LastConsolLegArrivalPort}");
}
Get New Documents¶
Retrieves recently added documents associated with shipments.
Parameters:
- input: Pagination and sorting options
Returns: List of documents created in the last 28 days with shipment details
Authorization: Requires HubPermissions.Shipment.View
Example:
var documents = await _shipmentAppService.GetNewDocuments(
new PagedAndSortedResultRequestDto
{
SkipCount = 0,
MaxResultCount = 50,
Sorting = "FileDate DESC"
}
);
foreach (var doc in documents.Items)
{
Console.WriteLine($"Document: {doc.FileName} (Shipment: {doc.ShipmentReference})");
}
Standard CRUD Operations¶
Get Single Shipment¶
Retrieves a single shipment by ID with all related data.
Example:
var shipment = await _shipmentAppService.GetAsync(shipmentId);
Console.WriteLine($"Shipment: {shipment.ShipmentReference}");
Console.WriteLine($"Departure: {shipment.EstimatedDeparture:d}");
Console.WriteLine($"Arrival: {shipment.EstimatedArrival:d}");
Get Shipment List¶
Task<PagedResultDtoWithFilters<ShipmentDto>> GetListAsync(
PagedAndSortedResultRequestWithFacetOptionsDto input
)
Retrieves a paginated list of shipments with optional filtering and faceting.
Example:
var shipments = await _shipmentAppService.GetListAsync(
new PagedAndSortedResultRequestWithFacetOptionsDto
{
SkipCount = 0,
MaxResultCount = 20,
Sorting = "CreationTime DESC",
IncludeFacets = true,
Filters = new Dictionary<string, List<string>>
{
{ "TransportMode", new List<string> { "Sea" } },
{ "ContainerMode", new List<string> { "FCL", "LCL" } }
}
}
);
// Display applied filters
foreach (var filter in shipments.AppliedFilters)
{
Console.WriteLine($"Filter: {filter.Field} = {string.Join(", ", filter.Values)}");
}
// Display facets for UI
foreach (var facet in shipments.Facets)
{
Console.WriteLine($"\nFacet: {facet.Field}");
foreach (var value in facet.Values)
{
Console.WriteLine($" {value.Value} ({value.Count})");
}
}
Create Shipment¶
Creates a new shipment.
Authorization: Requires HubPermissions.Shipment.Create
Example:
var newShipment = await _shipmentAppService.CreateAsync(
new CreateShipmentDto
{
ShipmentReference = "SHP-2024-001",
EstimatedDeparture = DateTime.UtcNow.AddDays(7),
EstimatedArrival = DateTime.UtcNow.AddDays(30),
FirstConsolLegDeparturePortId = portOfLoadingId,
LastConsolLegArrivalPortId = portOfDischargeId,
TransportModeId = seaTransportId,
ContainerModeId = fclModeId,
CargoDescription = "Electronics",
TotalPackages = 100,
TotalGrossWeight = 5000.0
}
);
Console.WriteLine($"Created shipment: {newShipment.Id}");
Update Shipment¶
Updates an existing shipment.
Authorization: Requires HubPermissions.Shipment.Edit
Example:
var updated = await _shipmentAppService.UpdateAsync(
shipmentId,
new UpdateShipmentDto
{
EstimatedDeparture = DateTime.UtcNow.AddDays(10),
EstimatedArrival = DateTime.UtcNow.AddDays(35),
TotalPackages = 120
}
);
Delete Shipment¶
Soft deletes a shipment (if auditing is enabled).
Authorization: Requires HubPermissions.Shipment.Delete
Example:
Domain Model¶
Shipment Entity¶
public class Shipment : FullAuditedAggregateRoot<Guid>, IMultiTenant
{
// Identification
public string ShipmentReference { get; set; }
public Guid? TenantId { get; set; }
// Dates
public DateTime? CargoReadyDate { get; set; }
public DateTime? EstimatedPickup { get; set; }
public DateTime? EstimatedDeparture { get; set; }
public DateTime? EstimatedArrival { get; set; }
public DateTime? ActualDeparture { get; set; }
public DateTime? ActualArrival { get; set; }
// Ports
public Guid? FirstConsolLegDeparturePortId { get; set; }
public Port FirstConsolLegDeparturePort { get; set; }
public Guid? LastConsolLegArrivalPortId { get; set; }
public Port LastConsolLegArrivalPort { get; set; }
// Transport
public Guid? TransportModeId { get; set; }
public CodeEntity TransportMode { get; set; }
public Guid? ContainerModeId { get; set; }
public CodeEntity ContainerMode { get; set; }
// Cargo
public string CargoDescription { get; set; }
public int? TotalPackages { get; set; }
public double? TotalGrossWeight { get; set; }
public double? TotalVolume { get; set; }
// Environmental
public double? Co2Emissions { get; set; }
// Relations
public ICollection<ShipmentAddress> Addresses { get; set; }
public ICollection<Document> Documents { get; set; }
public ICollection<Order> Orders { get; set; }
}
DTOs¶
ShipmentDto¶
public class ShipmentDto
{
public Guid Id { get; set; }
public string ShipmentReference { get; set; }
public DateTime? EstimatedDeparture { get; set; }
public DateTime? EstimatedArrival { get; set; }
public PortDto FirstConsolLegDeparturePort { get; set; }
public PortDto LastConsolLegArrivalPort { get; set; }
public CodeEntityDto TransportMode { get; set; }
public CodeEntityDto ContainerMode { get; set; }
public string CargoDescription { get; set; }
public int? TotalPackages { get; set; }
public double? TotalGrossWeight { get; set; }
// ... more properties
}
CreateShipmentDto¶
public class CreateShipmentDto
{
[Required]
[StringLength(100)]
public string ShipmentReference { get; set; }
public DateTime? EstimatedDeparture { get; set; }
public DateTime? EstimatedArrival { get; set; }
public Guid? FirstConsolLegDeparturePortId { get; set; }
public Guid? LastConsolLegArrivalPortId { get; set; }
public Guid? TransportModeId { get; set; }
public Guid? ContainerModeId { get; set; }
[StringLength(500)]
public string CargoDescription { get; set; }
[Range(0, int.MaxValue)]
public int? TotalPackages { get; set; }
[Range(0, double.MaxValue)]
public double? TotalGrossWeight { get; set; }
}
Filtering and Faceting¶
Organization Filtering¶
All shipment queries are automatically filtered by the current user's organization context:
protected override async Task<IQueryable<Shipment>> WithDetailsAsync()
{
var query = await base.WithDetailsAsync();
var filterCtx = Get<CurrentFilterContextProvider>();
// Apply organization filter
return query.ApplyOrganizationFilter(filterCtx);
}
Document Filtering¶
Documents can be conditionally included based on permissions:
return query.IncludeFilteredDocumentsIf(
filterCtx,
await AuthorizationService.IsGrantedAnyAsync(HubPermissions.Document.View)
);
Dynamic Facets¶
The service supports dynamic facet generation for: - Transport Mode - Container Mode - Departure Port - Arrival Port - Status - Organization
Performance Considerations¶
Tracking¶
For read-only operations, tracking is disabled:
using (shipmentRepository.DisableTracking())
{
var shipments = await shipmentRepository.GetListAsync();
// No change tracking overhead
}
Eager Loading¶
Related entities are eagerly loaded to avoid N+1 queries:
var queryable = await _repository.GetQueryableAsync();
queryable = queryable
.Include(s => s.FirstConsolLegDeparturePort)
.Include(s => s.LastConsolLegArrivalPort)
.Include(s => s.TransportMode)
.Include(s => s.ContainerMode);
Pagination¶
Always use pagination for large datasets:
Testing¶
Unit Test Example¶
public class ShipmentAppService_Tests : HubApplicationTestBase
{
private readonly IShipmentAppService _shipmentAppService;
private readonly IRepository<Shipment, Guid> _shipmentRepository;
public ShipmentAppService_Tests()
{
_shipmentAppService = GetRequiredService<IShipmentAppService>();
_shipmentRepository = GetRequiredService<IRepository<Shipment, Guid>>();
}
[Fact]
public async Task Should_Get_Upcoming_Shipments()
{
// Arrange
var testShipment = await _shipmentRepository.InsertAsync(
new Shipment
{
ShipmentReference = "TEST-001",
EstimatedDeparture = DateTime.UtcNow.AddDays(7),
EstimatedArrival = DateTime.UtcNow.AddDays(30)
}
);
// Act
var result = await _shipmentAppService.GetUpcomingShipments(
new PagedAndSortedResultRequestWithFacetOptionsDto
{
MaxResultCount = 10
}
);
// Assert
result.ShouldNotBeNull();
result.TotalCount.ShouldBeGreaterThan(0);
result.Items.ShouldContain(s => s.Id == testShipment.Id);
}
}
REST API Endpoints¶
The service is automatically exposed via REST API:
GET /api/hub/shipment/{id}
GET /api/hub/shipment
GET /api/hub/shipment/upcoming-shipments
GET /api/hub/shipment/new-documents
POST /api/hub/shipment
PUT /api/hub/shipment/{id}
DELETE /api/hub/shipment/{id}
Related Services¶
- Document Service - Managing shipment documents
- Organization Service - Organization context
- Order Service - Order management
Best Practices¶
- Always use pagination for list operations
- Apply organization filters to respect data isolation
- Use tracking selectively - disable for read-only operations
- Validate dates - ensure departure is before arrival
- Handle missing data - ports and transport modes may be optional
- Log important operations - especially create, update, delete
- Test with multiple organizations - verify isolation