Azure Table Storage
Azure Table Storage is used for storing image and video metadata, durable entity state data, and acquisition instance tracking. The Infrastructure package implements a dual-table index pattern for image metadata types — each image metadata type is backed by a primary table and an index table for efficient querying by alternate keys.
Registration
Table storage repositories are registered as part of the AddAlertCAStorageServices extension method:
services.AddAlertCAStorageServices(configuration);
This registers TableServiceClient via Azure.Extensions and all table repositories as scoped services. Index table repositories are injected into their corresponding primary repositories to enable automatic index maintenance on writes.
Configuration
{
"TableStorageOptions": {
"AccountName": "",
"SingleImageMetadataTableName": "",
"PanoImageMetadataTableName": "",
"PreStitchImageMetadataTableName": "",
"VideoMetadataTableName": "",
"ImageAcquisitionInstanceTableName": "",
"DurableEntityTableName": ""
}
}
Index table names are not configurable — each index repository uses a static TableName property (e.g., SingleImageMetadataIndexRepository.TableName = "SingleImageMetadataIndex").
Architecture
Note that video metadata does not have an index table — only single image, pano image, and pre-stitch image metadata use the dual-table pattern.
Dual-Table Index Pattern
Each image metadata entity has two tables. The primary repository's overridden CreateAsync automatically writes to both the primary and index tables:
- Primary table:
PartitionKey= CameraId, queries optimized for "get images for a camera in a time range" - Index table:
PartitionKey=yyyyMMddHHformat,RowKey=deviceId_reverseTicks— optimized for time-based batch lookups
Base Class: TableStorageRepository<T>
All table repositories inherit from TableStorageRepository<T> where T : BaseTableEntity:
| Method | Description |
|---|---|
CreateAsync(T entity) | Insert entity |
UpdateAsync(T entity) | Upsert entity |
DeleteAsync(string rowkey, string partitionKey) | Delete entity by keys |
DeleteBatchAsync(IEnumerable<T> entities) | Batch delete — groups by partition key, chunks of 100, parallel execution |
DeleteByQueryAsync(Expression<Func<T, bool>> predicate) | Query then batch delete matching entities |
GetByQueryAsync(Expression<Func<T, bool>> predicate) | Query with LINQ expressions |
GetLatestRow(Expression<Func<T, bool>> predicate) | Get single most recent entity matching predicate |
GetLatestRowsBySpecifiedCount(Expression<Func<T, bool>> predicate, int rowCount) | Get N most recent entities |
GetAllAsync() | Retrieve all entities |
BaseTableEntity implements ITableEntity and provides default PartitionKey, RowKey (GUID), Timestamp (UTC now), and ETag (All).
Repositories
SingleImageMetadataRepository
Stores metadata for individual camera capture images. Overrides CreateAsync to automatically add an index entry.
Interface: ISingleImageMetadataRepository extends ITableStorageRepository<SingleImageMetadataEntity>
Table name: "SingleImageMetadata" (hardcoded in constructor)
| Method | Description |
|---|---|
GetLatestImageMetadataByCameras(List<string> cameraIds) | Get latest image per camera (iterates each camera) |
GetSelectedImageMetadataByCamerasAndTimestamp(List<string> cameraIds, DateTimeOffset start, DateTimeOffset end) | Get first image per camera in time range |
GetLatestImageMetadataByCameraId(string cameraId) | Get single latest image |
GetImageMetadataByCameraIdAndTimeRange(string cameraId, DateTimeOffset start, DateTimeOffset end) | Get all images in time range |
GetRecentImagesByCameraId(string cameraId, int imageCount) | Get N most recent images |
AddIndexEntryAsync(SingleImageMetadataEntity entity) | Write index entry (called automatically by CreateAsync) |
PanoImageMetadataRepository
Stores metadata for stitched panoramic images. Overrides CreateAsync to auto-index.
Interface: IPanoImageMetadataRepository extends ITableStorageRepository<PanoImageMetadataEntity>
Table name: "PanoImageMetadata"
| Method | Description |
|---|---|
GetStitchedImagesByCameraInTimeRange(string cameraId, DateTimeOffset start, DateTimeOffset end) | Get pano images in time range |
GetLatestStitchedImageByCameraId(string cameraId) | Get latest pano image |
GetLatestClearDayImageByCamera(string cameraId) | Get latest image with IsClearDay == true |
AddIndexEntryAsync(PanoImageMetadataEntity entity) | Write index entry |
PreStitchImageMetadataRepository
Stores metadata for raw pre-stitch images (before panoramic stitching). Overrides CreateAsync to auto-index.
Interface: IPreStitchImageMetadataRepository extends ITableStorageRepository<PreStitchImageMetadataEntity>
Table name: "PreStitchImageMetadata"
| Method | Description |
|---|---|
GetImagesByCorrelationId(string cameraId, string correlationId) | Get images by PanoImageCorrelationId |
AddIndexEntryAsync(PreStitchImageMetadataEntity entity) | Write index entry |
Index Repositories
Each image metadata type has a corresponding index repository. All three share the same pattern — a static TableName and a single AddIndexEntryAsync method that creates a MetadataIndexEntity:
| Repository | Static Table Name |
|---|---|
SingleImageMetadataIndexRepository | "SingleImageMetadataIndex" |
PanoImageMetadataIndexRepository | "PanoImageMetadataIndex" |
PreStitchImageMetadataIndexRepository | "PreStitchImageMetadataIndex" |
public async Task AddIndexEntryAsync(DateTimeOffset timestamp, string rowkey, string deviceId)
{
var indexEntity = MetadataIndexEntity.Create(timestamp, rowkey, deviceId);
await base.CreateAsync(indexEntity);
}
VideoMetadataRepository
Stores metadata for video recordings. Does not inherit ITableStorageRepository<T> — uses a standalone interface. No index table.
Interface: IVideoMetadataRepository (standalone, not extending ITableStorageRepository)
Table name: configured via TableStorageOptions.VideoMetadataTableName
| Method | Description |
|---|---|
GetLatestVideoMetadataByCameraId(string cameraId) | Get latest video metadata |
GetVideoMetadataByCameraIdAndTimeRange(string cameraId, DateTimeOffset start, DateTimeOffset end) | Get videos in time range |
InsertVideoMetadata(VideoMetadataEntity metadata) | Insert video metadata (delegates to base CreateAsync) |
DurableEntityDataRepository
Stores state data for Azure Durable Entity functions — orchestration mode status, rates, and timing per device. Does not inherit ITableStorageRepository<T>.
Interface: IDurableEntityDataRepository (standalone)
Table name: configured via TableStorageOptions.DurableEntityTableName
The DurableEntityData entity has properties: DeviceId, CurrentConfig, SingleModeStatus/Rate, PanoModeStatus/Rate, LeasedModeStatus/Rate/EndDt, TurboModeStatus/Rate/EndDt, LastModifiedDt.
| Method | Description |
|---|---|
GetAllDurableEntities() | Get all durable entities |
GetLatestDurableEntityByDeviceId(string deviceId) | Get entity by device (partition key lookup) |
InsertOrMergeDurableEntity(DurableEntityData entity) | Upsert with TableUpdateMode.Merge |
DeleteDurableEntity(string deviceId) | Delete entity by device ID |
ImageAcquisitionInstanceRepository
Tracks image acquisition orchestration instances (recent run history).
Interface: IImageAcquisitionInstanceRepository (standalone)
Table name: configured via TableStorageOptions.ImageAcquisitionInstanceTableName
| Method | Description |
|---|---|
GetCurrentOrchestrationStatus(int minutes) | Get running instances from last N minutes (filters out @camerastate@ entries) |
Usage Example
using Alert.CA.Infrastructure.Repositories.TableStorageRepositories.Interfaces;
public class MetadataService(
ISingleImageMetadataRepository metadataRepo,
IPanoImageMetadataRepository panoRepo)
{
public async Task<IEnumerable<SingleImageMetadataEntity>> GetImagesForCamera(
string cameraId, DateTimeOffset start, DateTimeOffset end)
{
return await metadataRepo.GetImageMetadataByCameraIdAndTimeRange(
cameraId, start, end);
}
public async Task<PanoImageMetadataEntity?> GetLatestClearDay(string cameraId)
{
return await panoRepo.GetLatestClearDayImageByCamera(cameraId);
}
}